@@ -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
3235type 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}
0 commit comments