Skip to content

Commit e37741d

Browse files
authored
Replace webhook implementation with WebhookManagedBy. (#27)
1 parent e88557e commit e37741d

File tree

16 files changed

+650
-589
lines changed

16 files changed

+650
-589
lines changed

internal/anchor/validator.go

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import (
88

99
"github.com/go-logr/logr"
1010
k8sadm "k8s.io/api/admission/v1"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
12+
"k8s.io/apimachinery/pkg/runtime"
1113
"k8s.io/apimachinery/pkg/util/validation"
1214
"k8s.io/apimachinery/pkg/util/validation/field"
15+
ctrl "sigs.k8s.io/controller-runtime"
1316
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
1417

1518
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
@@ -30,45 +33,75 @@ const (
3033
// +kubebuilder:webhook:admissionReviewVersions=v1,path=/validate-hnc-x-k8s-io-v1alpha2-subnamespaceanchors,mutating=false,failurePolicy=fail,groups="hnc.x-k8s.io",resources=subnamespaceanchors,sideEffects=None,verbs=create;update;delete,versions=v1alpha2,name=subnamespaceanchors.hnc.x-k8s.io
3134

3235
type Validator struct {
33-
Log logr.Logger
34-
Forest *forest.Forest
35-
decoder admission.Decoder
36+
Log logr.Logger
37+
Forest *forest.Forest
3638
}
3739

38-
// req defines the aspects of the admission.Request that we care about.
39-
type anchorRequest struct {
40-
anchor *api.SubnamespaceAnchor
41-
op k8sadm.Operation
40+
func NewValidator(forest *forest.Forest) *Validator {
41+
return &Validator{
42+
Log: ctrl.Log.WithName("anchor").WithName("validate"),
43+
Forest: forest,
44+
}
4245
}
4346

44-
// Handle implements the validation webhook.
45-
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
46-
log := v.Log.WithValues("ns", req.Namespace, "nm", req.Name, "op", req.Operation, "user", req.UserInfo.Username)
47-
// Early exit since the HNC SA can do whatever it wants.
48-
if webhooks.IsHNCServiceAccount(&req.AdmissionRequest.UserInfo) {
47+
func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
48+
return v.validateAnchor(ctx, obj, k8sadm.Create)
49+
}
50+
51+
func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
52+
return v.validateAnchor(ctx, newObj, k8sadm.Update)
53+
}
54+
55+
func (v *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
56+
return v.validateAnchor(ctx, obj, k8sadm.Delete)
57+
}
58+
59+
func (v *Validator) validateAnchor(ctx context.Context, obj runtime.Object, operation k8sadm.Operation) (admission.Warnings, error) {
60+
req, err := admission.RequestFromContext(ctx)
61+
if err != nil {
62+
return nil, apierrors.NewInternalError(err)
63+
}
64+
65+
log := v.Log.WithValues("ns", req.Namespace, "nm", req.Name, "op", operation, "user", req.UserInfo.Username)
66+
67+
if webhooks.IsHNCServiceAccount(&req.UserInfo) {
4968
log.V(1).Info("Allowed change by HNC SA")
50-
return webhooks.Allow("HNC SA")
69+
return nil, nil
5170
}
5271

53-
decoded, err := v.decodeRequest(log, req)
54-
if err != nil {
55-
log.Error(err, "Couldn't decode request")
56-
return webhooks.DenyBadRequest(err)
72+
anchor, ok := obj.(*api.SubnamespaceAnchor)
73+
if !ok {
74+
return nil, apierrors.NewInternalError(fmt.Errorf("expected a SubnamespaceAnchor but got a %T", obj))
75+
}
76+
77+
anchorReq := &anchorRequest{
78+
anchor: anchor,
79+
op: operation,
5780
}
5881

59-
resp := v.handle(decoded)
60-
if !resp.Allowed {
61-
log.Info("Denied", "code", resp.Result.Code, "reason", resp.Result.Reason, "message", resp.Result.Message)
62-
} else {
63-
log.V(1).Info("Allowed", "message", resp.Result.Message)
82+
if err := v.handleValidation(anchorReq); err != nil {
83+
if !apierrors.IsInvalid(err) && !apierrors.IsConflict(err) && !apierrors.IsForbidden(err) {
84+
log.Error(err, "Validation failed")
85+
} else {
86+
log.Info("Denied", "reason", err)
87+
}
88+
return nil, err
6489
}
65-
return resp
90+
91+
log.V(1).Info("Allowed")
92+
return nil, nil
93+
}
94+
95+
// req defines the aspects of the admission.Request that we care about.
96+
type anchorRequest struct {
97+
anchor *api.SubnamespaceAnchor
98+
op k8sadm.Operation
6699
}
67100

68-
// handle implements the non-boilerplate logic of this validator, allowing it to be more easily unit
101+
// handleValidation implements the non-boilerplate logic of this validator, allowing it to be more easily unit
69102
// tested (ie without constructing a full admission.Request). It validates that the request is allowed
70103
// based on the current in-memory state of the forest.
71-
func (v *Validator) handle(req *anchorRequest) admission.Response {
104+
func (v *Validator) handleValidation(req *anchorRequest) error {
72105
v.Forest.Lock()
73106
defer v.Forest.Unlock()
74107
pnm := req.anchor.Namespace
@@ -83,28 +116,28 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
83116
allErrs := field.ErrorList{
84117
field.Invalid(fldPath, cnm, msg),
85118
}
86-
return webhooks.DenyInvalid(api.SubnamespaceAnchorGK, cnm, allErrs)
119+
return apierrors.NewInvalid(api.SubnamespaceAnchorGK, cnm, allErrs)
87120
}
88121
}
89122

90123
labelErrs := config.ValidateManagedLabels(req.anchor.Spec.Labels)
91124
annotationErrs := config.ValidateManagedAnnotations(req.anchor.Spec.Annotations)
92125
allErrs := append(labelErrs, annotationErrs...)
93126
if len(allErrs) > 0 {
94-
return webhooks.DenyInvalid(api.SubnamespaceAnchorGK, req.anchor.Name, allErrs)
127+
return apierrors.NewInvalid(api.SubnamespaceAnchorGK, req.anchor.Name, allErrs)
95128
}
96129

97130
switch req.op {
98131
case k8sadm.Create:
99132
// Can't create subnamespaces in unmanaged namespaces
100133
if why := config.WhyUnmanaged(pnm); why != "" {
101134
err := fmt.Errorf("cannot create a subnamespace in the unmanaged namespace %q (%s)", pnm, why)
102-
return webhooks.DenyForbidden(api.SubnamespaceAnchorGR, pnm, err)
135+
return apierrors.NewForbidden(api.SubnamespaceAnchorGR, pnm, err)
103136
}
104137
// Can't create subnamespaces using unmanaged namespace names
105138
if why := config.WhyUnmanaged(cnm); why != "" {
106139
err := fmt.Errorf("cannot create a subnamespace using the unmanaged namespace name %q (%s)", cnm, why)
107-
return webhooks.DenyForbidden(api.SubnamespaceAnchorGR, cnm, err)
140+
return apierrors.NewForbidden(api.SubnamespaceAnchorGR, cnm, err)
108141
}
109142

110143
// Can't create anchors for existing namespaces, _unless_ it's for a subns with a missing
@@ -113,7 +146,7 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
113146
childIsMissingAnchor := (cns.Parent().Name() == pnm && cns.IsSub)
114147
if !childIsMissingAnchor {
115148
err := errors.New("cannot create a subnamespace using an existing namespace")
116-
return webhooks.DenyConflict(api.SubnamespaceAnchorGR, cnm, err)
149+
return apierrors.NewConflict(api.SubnamespaceAnchorGR, cnm, err)
117150
}
118151
}
119152

@@ -122,40 +155,12 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
122155
// unless allowCascadingDeletion is set.
123156
if req.anchor.Status.State == api.Ok && cns.ChildNames() != nil && !cns.AllowsCascadingDeletion() {
124157
err := fmt.Errorf("subnamespace %s is not a leaf and doesn't allow cascading deletion. Please set allowCascadingDeletion flag or make it a leaf first", cnm)
125-
return webhooks.DenyForbidden(api.SubnamespaceAnchorGR, cnm, err)
158+
return apierrors.NewForbidden(api.SubnamespaceAnchorGR, cnm, err)
126159
}
127160

128161
default:
129162
// nop for updates etc
130163
}
131164

132-
return webhooks.Allow("")
133-
}
134-
135-
// decodeRequest gets the information we care about into a simple struct that's easy to both a) use
136-
// and b) factor out in unit tests.
137-
func (v *Validator) decodeRequest(log logr.Logger, in admission.Request) (*anchorRequest, error) {
138-
anchor := &api.SubnamespaceAnchor{}
139-
var err error
140-
// For DELETE request, use DecodeRaw() from req.OldObject, since Decode() only
141-
// uses req.Object, which will be empty for a DELETE request.
142-
if in.Operation == k8sadm.Delete {
143-
log.V(1).Info("Decoding a delete request.")
144-
err = v.decoder.DecodeRaw(in.OldObject, anchor)
145-
} else {
146-
err = v.decoder.Decode(in, anchor)
147-
}
148-
if err != nil {
149-
return nil, err
150-
}
151-
152-
return &anchorRequest{
153-
anchor: anchor,
154-
op: in.Operation,
155-
}, nil
156-
}
157-
158-
func (v *Validator) InjectDecoder(d admission.Decoder) error {
159-
v.decoder = d
160165
return nil
161166
}

internal/anchor/validator_test.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
. "github.com/onsi/gomega"
77
k8sadm "k8s.io/api/admission/v1"
8-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
98

109
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
1110
"sigs.k8s.io/hierarchical-namespaces/internal/config"
@@ -47,11 +46,14 @@ func TestCreateSubnamespaces(t *testing.T) {
4746
}
4847

4948
// Test
50-
got := v.handle(req)
49+
err := v.handleValidation(req)
5150

5251
// Report
53-
logResult(t, got.AdmissionResponse.Result)
54-
g.Expect(got.AdmissionResponse.Allowed).ShouldNot(Equal(tc.fail))
52+
if tc.fail {
53+
g.Expect(err).Should(HaveOccurred())
54+
} else {
55+
g.Expect(err).ShouldNot(HaveOccurred())
56+
}
5557
})
5658
}
5759
}
@@ -82,11 +84,14 @@ func TestDeleteSubnamespaces(t *testing.T) {
8284
}
8385

8486
// Test
85-
got := v.handle(req)
87+
err := v.handleValidation(req)
8688

8789
// Report
88-
logResult(t, got.AdmissionResponse.Result)
89-
g.Expect(got.AdmissionResponse.Allowed).ShouldNot(Equal(tc.fail))
90+
if tc.fail {
91+
g.Expect(err).Should(HaveOccurred())
92+
} else {
93+
g.Expect(err).ShouldNot(HaveOccurred())
94+
}
9095
})
9196
}
9297
}
@@ -152,11 +157,14 @@ func TestManagedMeta(t *testing.T) {
152157
}
153158

154159
// Test
155-
got := v.handle(req)
160+
err := v.handleValidation(req)
156161

157162
// Report
158-
logResult(t, got.AdmissionResponse.Result)
159-
g.Expect(got.AdmissionResponse.Allowed).Should(Equal(tc.allowed))
163+
if tc.allowed {
164+
g.Expect(err).ShouldNot(HaveOccurred())
165+
} else {
166+
g.Expect(err).Should(HaveOccurred())
167+
}
160168
})
161169
}
162170
}
@@ -207,15 +215,14 @@ func TestAllowCascadingDeleteSubnamespaces(t *testing.T) {
207215
}
208216

209217
// Test
210-
got := v.handle(req)
218+
err := v.handleValidation(req)
211219

212220
// Report
213-
logResult(t, got.AdmissionResponse.Result)
214-
g.Expect(got.AdmissionResponse.Allowed).ShouldNot(Equal(tc.fail))
221+
if tc.fail {
222+
g.Expect(err).Should(HaveOccurred())
223+
} else {
224+
g.Expect(err).ShouldNot(HaveOccurred())
225+
}
215226
})
216227
}
217228
}
218-
219-
func logResult(t *testing.T, result *metav1.Status) {
220-
t.Logf("Got reason %q, code %d, msg %q", result.Reason, result.Code, result.Message)
221-
}

0 commit comments

Comments
 (0)