@@ -7,15 +7,15 @@ import (
77 "strings"
88
99 "github.com/go-logr/logr"
10- k8sadm "k8s.io/api/admission/v1"
1110 authnv1 "k8s.io/api/authentication/v1"
1211 authzv1 "k8s.io/api/authorization/v1"
1312 corev1 "k8s.io/api/core/v1"
1413 apierrors "k8s.io/apimachinery/pkg/api/errors"
15- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1 "
14+ "k8s.io/apimachinery/pkg/runtime "
1615 "k8s.io/apimachinery/pkg/runtime/schema"
1716 "k8s.io/apimachinery/pkg/types"
1817 "sigs.k8s.io/controller-runtime/pkg/client"
18+ logf "sigs.k8s.io/controller-runtime/pkg/log"
1919 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2020
2121 api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
@@ -38,10 +38,15 @@ const (
3838// +kubebuilder:webhook:admissionReviewVersions=v1,path=/validate-hnc-x-k8s-io-v1alpha2-hierarchyconfigurations,mutating=false,failurePolicy=fail,groups="hnc.x-k8s.io",resources=hierarchyconfigurations,sideEffects=None,verbs=create;update,versions=v1alpha2,name=hierarchyconfigurations.hnc.x-k8s.io
3939
4040type Validator struct {
41- Log logr.Logger
42- Forest * forest.Forest
43- server serverClient
44- decoder admission.Decoder
41+ Forest * forest.Forest
42+ server serverClient
43+ }
44+
45+ func NewValidator (forest * forest.Forest , client client.Client ) * Validator {
46+ return & Validator {
47+ Forest : forest ,
48+ server : & realClient {client : client },
49+ }
4550}
4651
4752// serverClient represents the checks that should typically be performed against the apiserver, but
@@ -55,13 +60,19 @@ type serverClient interface {
5560 IsAdmin (ctx context.Context , ui * authnv1.UserInfo , nnm string ) (bool , error )
5661}
5762
58- // request defines the aspects of the admission.Request that we care about.
59- type request struct {
60- hc * api.HierarchyConfiguration
61- ui * authnv1.UserInfo
63+ func (v * Validator ) ValidateCreate (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
64+ return v .handle (ctx , obj )
65+ }
66+
67+ func (v * Validator ) ValidateDelete (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
68+ return v .handle (ctx , obj )
6269}
6370
64- // Handle implements the validation webhook.
71+ func (v * Validator ) ValidateUpdate (ctx context.Context , oldObj , newObj runtime.Object ) (admission.Warnings , error ) {
72+ return v .handle (ctx , newObj )
73+ }
74+
75+ // handle implements the validation webhook.
6576//
6677// During updates, the validator currently ignores the existing state of the object (`oldObject`).
6778// The reason is that most of the checks being performed are on the state of the entire forest, not
@@ -84,25 +95,6 @@ type request struct {
8495// possible to introduce structural failures, as described in the user docs.
8596//
8697// Authz false positives are prevented as described by the comments to `getServerChecks`.
87- func (v * Validator ) Handle (ctx context.Context , req admission.Request ) admission.Response {
88- log := v .Log .WithValues ("ns" , req .Namespace , "op" , req .Operation , "user" , req .UserInfo .Username )
89- decoded , err := v .decodeRequest (req )
90- if err != nil {
91- log .Error (err , "Couldn't decode request" )
92- return webhooks .DenyBadRequest (err )
93- }
94-
95- resp := v .handle (ctx , log , decoded )
96- if ! resp .Allowed {
97- log .Info ("Denied" , "code" , resp .Result .Code , "reason" , resp .Result .Reason , "message" , resp .Result .Message )
98- } else {
99- log .V (1 ).Info ("Allowed" , "message" , resp .Result .Message )
100- }
101- return resp
102- }
103-
104- // handle implements the non-boilerplate logic of this validator, allowing it to be more easily unit
105- // tested (ie without constructing a full admission.Request).
10698//
10799// This follows the standard HNC pattern of:
108100// - Load a bunch of stuff from the apiserver
@@ -111,47 +103,57 @@ func (v *Validator) Handle(ctx context.Context, req admission.Request) admission
111103//
112104// This minimizes the amount of time that the forest is locked, allowing different threads to
113105// proceed in parallel.
114- func (v * Validator ) handle (ctx context.Context , log logr.Logger , req * request ) admission.Response {
106+ func (v * Validator ) handle (ctx context.Context , obj runtime.Object ) (admission.Warnings , error ) {
107+ req , err := admission .RequestFromContext (ctx )
108+ if err != nil {
109+ return nil , apierrors .NewInternalError (err )
110+ }
111+
112+ log := admission .DefaultLogConstructor (logf .FromContext (ctx ), & req )
113+
115114 // Early exit: the HNC SA can do whatever it wants. This is because if an illegal HC already
116115 // exists on the K8s server, we need to be able to update its status even though the rest of the
117116 // object wouldn't pass legality. We should probably only give the HNC SA the ability to modify
118117 // the _status_, though. TODO: https://github.com/kubernetes-sigs/hierarchical-namespaces/issues/80.
119- if webhooks .IsHNCServiceAccount (req .ui ) {
120- return allow ( "HNC SA" )
118+ if webhooks .IsHNCServiceAccount (& req .UserInfo ) {
119+ return nil , nil
121120 }
122121
123- if why := config . WhyUnmanaged ( req . hc . Namespace ); why != "" {
124- err := fmt . Errorf ( "namespace %q is not managed by HNC (%s) and cannot be set as a child of another namespace" , req . hc . Namespace , why )
125- return webhooks . DenyForbidden ( api . HierarchyConfigurationGR , api . Singleton , err )
122+ hc , ok := obj .( * api. HierarchyConfiguration )
123+ if ! ok {
124+ return nil , apierrors . NewInternalError ( fmt . Errorf ( "expected a HierarchyConfiguration but got a %T" , obj ) )
126125 }
127- if why := config .WhyUnmanaged (req .hc .Spec .Parent ); why != "" {
128- err := fmt .Errorf ("namespace %q is not managed by HNC (%s) and cannot be set as the parent of another namespace" , req .hc .Spec .Parent , why )
129- return webhooks .DenyForbidden (api .HierarchyConfigurationGR , api .Singleton , err )
126+
127+ if why := config .WhyUnmanaged (hc .Namespace ); why != "" {
128+ return nil , apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("namespace %q is not managed by HNC (%s) and cannot be set as a child of another namespace" , hc .Namespace , why ))
129+ }
130+ if why := config .WhyUnmanaged (hc .Spec .Parent ); why != "" {
131+ return nil , apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("namespace %q is not managed by HNC (%s) and cannot be set as the parent of another namespace" , hc .Spec .Parent , why ))
130132 }
131133
132- labelErrs := config .ValidateManagedLabels (req . hc .Spec .Labels )
133- annotationErrs := config .ValidateManagedAnnotations (req . hc .Spec .Annotations )
134+ labelErrs := config .ValidateManagedLabels (hc .Spec .Labels )
135+ annotationErrs := config .ValidateManagedAnnotations (hc .Spec .Annotations )
134136 allErrs := append (labelErrs , annotationErrs ... )
135137 if len (allErrs ) > 0 {
136- return webhooks . DenyInvalid (api .HierarchyConfigurationGK , req . hc .Name , allErrs )
138+ return nil , apierrors . NewInvalid (api .HierarchyConfigurationGK , hc .Name , allErrs )
137139 }
138140
139141 // Do all checks that require holding the in-memory lock. Generate a list of server checks we
140142 // should perform once the lock is released.
141- serverChecks , resp := v .checkForest (req . hc )
142- if ! resp . Allowed {
143- return resp
143+ serverChecks , err := v .checkForest (hc )
144+ if err != nil {
145+ return nil , err
144146 }
145147
146148 // Ensure that the server's in the right state to make the changes.
147- return v .checkServer (ctx , log , req .ui , serverChecks )
149+ return nil , v .checkServer (ctx , log , & req .UserInfo , serverChecks )
148150}
149151
150152// checkForest validates that the request is allowed based on the current in-memory state of the
151153// forest. If it is, it returns a list of checks we need to perform against the apiserver in order
152154// to be allowed to make the change; these checks are executed _after_ the in-memory lock is
153155// released.
154- func (v * Validator ) checkForest (hc * api.HierarchyConfiguration ) ([]serverCheck , admission. Response ) {
156+ func (v * Validator ) checkForest (hc * api.HierarchyConfiguration ) ([]serverCheck , error ) {
155157 v .Forest .Lock ()
156158 defer v .Forest .Unlock ()
157159
@@ -161,59 +163,54 @@ func (v *Validator) checkForest(hc *api.HierarchyConfiguration) ([]serverCheck,
161163 newParent := v .Forest .Get (hc .Spec .Parent )
162164
163165 // Check problems on the namespace itself
164- if resp := v .checkNS (ns ); ! resp . Allowed {
165- return nil , resp
166+ if err := v .checkNS (ns ); err != nil {
167+ return nil , err
166168 }
167169
168170 // Check problems on the parents
169- if resp := v .checkParent (ns , curParent , newParent ); ! resp . Allowed {
170- return nil , resp
171+ if err := v .checkParent (ns , curParent , newParent ); err != nil {
172+ return nil , err
171173 }
172174
173175 // The structure looks good. Get the list of namespaces we need server checks on.
174- return v .getServerChecks (curParent , newParent ), allow ( "" )
176+ return v .getServerChecks (curParent , newParent ), nil
175177}
176178
177179// checkNS looks for problems with the current namespace that should prevent changes.
178- func (v * Validator ) checkNS (ns * forest.Namespace ) admission. Response {
180+ func (v * Validator ) checkNS (ns * forest.Namespace ) error {
179181 // Wait until the namespace has been synced
180182 if ! ns .Exists () {
181- msg := fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , ns .Name ())
182- return webhooks .DenyServiceUnavailable (msg )
183+ return apierrors .NewServiceUnavailable (fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , ns .Name ()))
183184 }
184185
185186 // Deny the request if the namespace has a halted root - but not if it's halted itself, since we
186187 // may be trying to resolve the halted condition.
187188 haltedRoot := ns .GetHaltedRoot ()
188189 if haltedRoot != "" && haltedRoot != ns .Name () {
189- err := fmt .Errorf ("ancestor %q of namespace %q has a critical condition, which must be resolved before any changes can be made to the hierarchy configuration" , haltedRoot , ns .Name ())
190- return webhooks .DenyForbidden (api .HierarchyConfigurationGR , api .Singleton , err )
190+ return apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("ancestor %q of namespace %q has a critical condition, which must be resolved before any changes can be made to the hierarchy configuration" , haltedRoot , ns .Name ()))
191191 }
192192
193- return allow ( "" )
193+ return nil
194194}
195195
196196// checkParent validates if the parent is legal based on the current in-memory state of the forest.
197- func (v * Validator ) checkParent (ns , curParent , newParent * forest.Namespace ) admission. Response {
197+ func (v * Validator ) checkParent (ns , curParent , newParent * forest.Namespace ) error {
198198 if ns .IsExternal () && newParent != nil {
199- err := fmt .Errorf ("namespace %q is managed by %q, not HNC, so it cannot have a parent in HNC" , ns .Name (), ns .Manager )
200- return webhooks .DenyForbidden (api .HierarchyConfigurationGR , api .Singleton , err )
199+ return apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("namespace %q is managed by %q, not HNC, so it cannot have a parent in HNC" , ns .Name (), ns .Manager ))
201200 }
202201
203202 if curParent == newParent {
204- return allow ( "parent unchanged" )
203+ return nil
205204 }
206205
207206 // Prevent changing parent of a subnamespace
208207 if ns .IsSub {
209- err := fmt .Errorf ("illegal parent: Cannot set the parent of %q to %q because it's a subnamespace of %q" , ns .Name (), newParent .Name (), curParent .Name ())
210- return webhooks .DenyConflict (api .HierarchyConfigurationGR , api .Singleton , err )
208+ return apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("illegal parent: Cannot set the parent of %q to %q because it's a subnamespace of %q" , ns .Name (), newParent .Name (), curParent .Name ()))
211209 }
212210
213211 // non existence of parent namespace -> not allowed
214212 if newParent != nil && ! newParent .Exists () {
215- err := fmt .Errorf ("requested parent %q does not exist" , newParent .Name ())
216- return webhooks .DenyForbidden (api .HierarchyConfigurationGR , api .Singleton , err )
213+ return apierrors .NewForbidden (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("requested parent %q does not exist" , newParent .Name ()))
217214 }
218215
219216 // Is this change structurally legal? Note that this can "leak" information about the hierarchy
@@ -222,20 +219,18 @@ func (v *Validator) checkParent(ns, curParent, newParent *forest.Namespace) admi
222219 // have visibility into its ancestry and descendents, and this check can only fail if the new
223220 // parent conflicts with something in the _existing_ hierarchy.
224221 if reason := ns .CanSetParent (newParent ); reason != "" {
225- err := fmt .Errorf ("illegal parent: %s" , reason )
226- return webhooks .DenyConflict (api .HierarchyConfigurationGR , api .Singleton , err )
222+ return apierrors .NewConflict (api .HierarchyConfigurationGR , api .Singleton , fmt .Errorf ("illegal parent: %s" , reason ))
227223 }
228224
229225 // Prevent overwriting source objects in the descendants after the hierarchy change.
230226 if co := v .getConflictingObjects (newParent , ns ); len (co ) != 0 {
231227 msg := "Cannot update hierarchy because it would overwrite the following object(s):\n "
232228 msg += " * " + strings .Join (co , "\n * " ) + "\n "
233229 msg += "To fix this, please rename or remove the conflicting objects first."
234- err := errors .New (msg )
235- return webhooks .DenyConflict (api .HierarchyConfigurationGR , api .Singleton , err )
230+ return apierrors .NewConflict (api .HierarchyConfigurationGR , api .Singleton , errors .New (msg ))
236231 }
237232
238- return allow ( "" )
233+ return nil
239234}
240235
241236// getConflictingObjects returns a list of namespaced objects if there's any conflict.
@@ -393,9 +388,9 @@ func (v *Validator) getServerChecks(curParent, newParent *forest.Namespace) []se
393388}
394389
395390// checkServer executes the list of requested checks.
396- func (v * Validator ) checkServer (ctx context.Context , log logr.Logger , ui * authnv1.UserInfo , reqs []serverCheck ) admission. Response {
391+ func (v * Validator ) checkServer (ctx context.Context , log logr.Logger , ui * authnv1.UserInfo , reqs []serverCheck ) error {
397392 if v .server == nil {
398- return allow ( "" ) // unit test; TODO put in fake
393+ return nil // unit test; TODO put in fake
399394 }
400395
401396 // TODO: parallelize?
@@ -405,54 +400,26 @@ func (v *Validator) checkServer(ctx context.Context, log logr.Logger, ui *authnv
405400 log .Info ("Checking existence" , "object" , req .nnm , "reason" , req .reason )
406401 exists , err := v .server .Exists (ctx , req .nnm )
407402 if err != nil {
408- err = fmt .Errorf ("while checking existance for %q, the %s: %w" , req .nnm , req .reason , err )
409- return webhooks .DenyInternalError (err )
403+ return apierrors .NewInternalError (fmt .Errorf ("while checking existance for %q, the %s: %w" , req .nnm , req .reason , err ))
410404 }
411405
412406 if exists {
413- msg := fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , req .nnm )
414- return webhooks .DenyServiceUnavailable (msg )
407+ return apierrors .NewServiceUnavailable (fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , req .nnm ))
415408 }
416409
417410 case checkAuthz :
418411 log .Info ("Checking authz" , "object" , req .nnm , "reason" , req .reason )
419412 allowed , err := v .server .IsAdmin (ctx , ui , req .nnm )
420413 if err != nil {
421- err = fmt .Errorf ("while checking authz for %q, the %s: %w" , req .nnm , req .reason , err )
422- return webhooks .DenyInternalError (err )
414+ return apierrors .NewInternalError (fmt .Errorf ("while checking authz for %q, the %s: %w" , req .nnm , req .reason , err ))
423415 }
424416
425417 if ! allowed {
426- return webhooks .DenyUnauthorized (fmt .Sprintf ("User %s is not authorized to modify the subtree of %s, which is the %s" ,
427- ui .Username , req .nnm , req .reason ))
418+ return apierrors .NewUnauthorized (fmt .Sprintf ("user %s is not authorized to modify the subtree of %s, which is the %s" , ui .Username , req .nnm , req .reason ))
428419 }
429420 }
430421 }
431422
432- return allow ("" )
433- }
434-
435- // decodeRequest gets the information we care about into a simple struct that's easy to both a) use
436- // and b) factor out in unit tests.
437- func (v * Validator ) decodeRequest (in admission.Request ) (* request , error ) {
438- hc := & api.HierarchyConfiguration {}
439- if err := v .decoder .Decode (in , hc ); err != nil {
440- return nil , err
441- }
442-
443- return & request {
444- hc : hc ,
445- ui : & in .UserInfo ,
446- }, nil
447- }
448-
449- func (v * Validator ) InjectClient (c client.Client ) error {
450- v .server = & realClient {client : c }
451- return nil
452- }
453-
454- func (v * Validator ) InjectDecoder (d admission.Decoder ) error {
455- v .decoder = d
456423 return nil
457424}
458425
@@ -505,15 +472,3 @@ func (r *realClient) IsAdmin(ctx context.Context, ui *authnv1.UserInfo, nnm stri
505472 // Extract the interesting result
506473 return sar .Status .Allowed , err
507474}
508-
509- // allow is a replacement for controller-runtime's admission.Allowed() that allows you to set the
510- // message (human-readable) as opposed to the reason (machine-readable).
511- func allow (msg string ) admission.Response {
512- return admission.Response {AdmissionResponse : k8sadm.AdmissionResponse {
513- Allowed : true ,
514- Result : & metav1.Status {
515- Code : 0 ,
516- Message : msg ,
517- },
518- }}
519- }
0 commit comments