@@ -15,14 +15,14 @@ import (
15
15
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16
16
"k8s.io/apimachinery/pkg/runtime/schema"
17
17
"k8s.io/apimachinery/pkg/types"
18
- "k8s.io/apimachinery/pkg/util/validation/field"
19
18
"sigs.k8s.io/controller-runtime/pkg/client"
20
19
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
21
20
22
21
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
23
22
"sigs.k8s.io/hierarchical-namespaces/internal/config"
24
23
"sigs.k8s.io/hierarchical-namespaces/internal/forest"
25
24
"sigs.k8s.io/hierarchical-namespaces/internal/selectors"
25
+ "sigs.k8s.io/hierarchical-namespaces/internal/webhooks"
26
26
)
27
27
28
28
const (
@@ -89,7 +89,7 @@ func (v *Validator) Handle(ctx context.Context, req admission.Request) admission
89
89
decoded , err := v .decodeRequest (req )
90
90
if err != nil {
91
91
log .Error (err , "Couldn't decode request" )
92
- return deny (metav1 .StatusReasonBadRequest , err .Error ())
92
+ return webhooks . Deny (metav1 .StatusReasonBadRequest , err .Error ())
93
93
}
94
94
95
95
resp := v .handle (ctx , log , decoded )
@@ -122,11 +122,11 @@ func (v *Validator) handle(ctx context.Context, log logr.Logger, req *request) a
122
122
123
123
if why := config .WhyUnmanaged (req .hc .Namespace ); why != "" {
124
124
reason := fmt .Sprintf ("Namespace %q is not managed by HNC (%s) and cannot be set as a child of another namespace" , req .hc .Namespace , why )
125
- return deny (metav1 .StatusReasonForbidden , reason )
125
+ return webhooks . Deny (metav1 .StatusReasonForbidden , reason )
126
126
}
127
127
if why := config .WhyUnmanaged (req .hc .Spec .Parent ); why != "" {
128
128
reason := fmt .Sprintf ("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 deny (metav1 .StatusReasonForbidden , reason )
129
+ return webhooks . Deny (metav1 .StatusReasonForbidden , reason )
130
130
}
131
131
132
132
// Do all checks that require holding the in-memory lock. Generate a list of server checks we
@@ -172,15 +172,15 @@ func (v *Validator) checkNS(ns *forest.Namespace) admission.Response {
172
172
// Wait until the namespace has been synced
173
173
if ! ns .Exists () {
174
174
msg := fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , ns .Name ())
175
- return deny (metav1 .StatusReasonServiceUnavailable , msg )
175
+ return webhooks . Deny (metav1 .StatusReasonServiceUnavailable , msg )
176
176
}
177
177
178
178
// Deny the request if the namespace has a halted root - but not if it's halted itself, since we
179
179
// may be trying to resolve the halted condition.
180
180
haltedRoot := ns .GetHaltedRoot ()
181
181
if haltedRoot != "" && haltedRoot != ns .Name () {
182
182
msg := fmt .Sprintf ("The 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 ())
183
- return deny (metav1 .StatusReasonForbidden , msg )
183
+ return webhooks . Deny (metav1 .StatusReasonForbidden , msg )
184
184
}
185
185
186
186
return allow ("" )
@@ -190,7 +190,7 @@ func (v *Validator) checkNS(ns *forest.Namespace) admission.Response {
190
190
func (v * Validator ) checkParent (ns , curParent , newParent * forest.Namespace ) admission.Response {
191
191
if ns .IsExternal () && newParent != nil {
192
192
msg := fmt .Sprintf ("Namespace %q is managed by %q, not HNC, so it cannot have a parent in HNC." , ns .Name (), ns .Manager )
193
- return deny (metav1 .StatusReasonForbidden , msg )
193
+ return webhooks . Deny (metav1 .StatusReasonForbidden , msg )
194
194
}
195
195
196
196
if curParent == newParent {
@@ -200,12 +200,12 @@ func (v *Validator) checkParent(ns, curParent, newParent *forest.Namespace) admi
200
200
// Prevent changing parent of a subnamespace
201
201
if ns .IsSub {
202
202
reason := fmt .Sprintf ("Cannot set the parent of %q to %q because it's a subnamespace of %q" , ns .Name (), newParent .Name (), curParent .Name ())
203
- return deny (metav1 .StatusReasonConflict , "Illegal parent: " + reason )
203
+ return webhooks . Deny (metav1 .StatusReasonConflict , "Illegal parent: " + reason )
204
204
}
205
205
206
206
// non existence of parent namespace -> not allowed
207
207
if newParent != nil && ! newParent .Exists () {
208
- return deny (metav1 .StatusReasonForbidden , "The requested parent " + newParent .Name ()+ " does not exist" )
208
+ return webhooks . Deny (metav1 .StatusReasonForbidden , "The requested parent " + newParent .Name ()+ " does not exist" )
209
209
}
210
210
211
211
// Is this change structurally legal? Note that this can "leak" information about the hierarchy
@@ -214,15 +214,15 @@ func (v *Validator) checkParent(ns, curParent, newParent *forest.Namespace) admi
214
214
// have visibility into its ancestry and descendents, and this check can only fail if the new
215
215
// parent conflicts with something in the _existing_ hierarchy.
216
216
if reason := ns .CanSetParent (newParent ); reason != "" {
217
- return deny (metav1 .StatusReasonConflict , "Illegal parent: " + reason )
217
+ return webhooks . Deny (metav1 .StatusReasonConflict , "Illegal parent: " + reason )
218
218
}
219
219
220
220
// Prevent overwriting source objects in the descendants after the hierarchy change.
221
221
if co := v .getConflictingObjects (newParent , ns ); len (co ) != 0 {
222
222
msg := "Cannot update hierarchy because it would overwrite the following object(s):\n "
223
223
msg += " * " + strings .Join (co , "\n * " ) + "\n "
224
224
msg += "To fix this, please rename or remove the conflicting objects first."
225
- return deny (metav1 .StatusReasonConflict , msg )
225
+ return webhooks . Deny (metav1 .StatusReasonConflict , msg )
226
226
}
227
227
228
228
return allow ("" )
@@ -394,23 +394,23 @@ func (v *Validator) checkServer(ctx context.Context, log logr.Logger, ui *authnv
394
394
log .Info ("Checking existance" , "object" , req .nnm , "reason" , req .reason )
395
395
exists , err := v .server .Exists (ctx , req .nnm )
396
396
if err != nil {
397
- return deny (metav1 .StatusReasonUnknown , fmt .Sprintf ("while checking existance for %q, the %s: %s" , req .nnm , req .reason , err ))
397
+ return webhooks . Deny (metav1 .StatusReasonUnknown , fmt .Sprintf ("while checking existance for %q, the %s: %s" , req .nnm , req .reason , err ))
398
398
}
399
399
400
400
if exists {
401
401
msg := fmt .Sprintf ("HNC has not reconciled namespace %q yet - please try again in a few moments." , req .nnm )
402
- return deny (metav1 .StatusReasonServiceUnavailable , msg )
402
+ return webhooks . Deny (metav1 .StatusReasonServiceUnavailable , msg )
403
403
}
404
404
405
405
case checkAuthz :
406
406
log .Info ("Checking authz" , "object" , req .nnm , "reason" , req .reason )
407
407
allowed , err := v .server .IsAdmin (ctx , ui , req .nnm )
408
408
if err != nil {
409
- return deny (metav1 .StatusReasonUnknown , fmt .Sprintf ("while checking authz for %q, the %s: %s" , req .nnm , req .reason , err ))
409
+ return webhooks . Deny (metav1 .StatusReasonUnknown , fmt .Sprintf ("while checking authz for %q, the %s: %s" , req .nnm , req .reason , err ))
410
410
}
411
411
412
412
if ! allowed {
413
- return deny (metav1 .StatusReasonUnauthorized , fmt .Sprintf ("User %s is not authorized to modify the subtree of %s, which is the %s" ,
413
+ return webhooks . Deny (metav1 .StatusReasonUnauthorized , fmt .Sprintf ("User %s is not authorized to modify the subtree of %s, which is the %s" ,
414
414
ui .Username , req .nnm , req .reason ))
415
415
}
416
416
}
@@ -525,69 +525,3 @@ func allow(msg string) admission.Response {
525
525
},
526
526
}}
527
527
}
528
-
529
- // deny is a replacement for controller-runtime's admission.Denied() that allows you to set _both_ a
530
- // human-readable message _and_ a machine-readable reason, and also sets the code correctly instead
531
- // of hardcoding it to 403 Forbidden.
532
- func deny (reason metav1.StatusReason , msg string ) admission.Response {
533
- return admission.Response {AdmissionResponse : k8sadm.AdmissionResponse {
534
- Allowed : false ,
535
- Result : & metav1.Status {
536
- Code : codeFromReason (reason ),
537
- Message : msg ,
538
- Reason : reason ,
539
- }},
540
- }
541
- }
542
-
543
- // denyInvalid is a wrapper for deny with reason metav1.StatusReasonInvalid
544
- func denyInvalid (field * field.Path , msg string ) admission.Response {
545
- // We need to set the custom message in both Details and Message fields.
546
- //
547
- // When manipulating the HNC configuration object via kubectl directly, kubectl
548
- // ignores the Message field and displays the Details field if an error is
549
- // StatusReasonInvalid (see implementation here: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/util/helpers.go#L145-L160).
550
- //
551
- // When manipulating the HNC configuration object via the hns kubectl plugin,
552
- // if an error is StatusReasonInvalid, only the Message field will be displayed. This is because
553
- // the Error method (https://github.com/kubernetes/client-go/blob/cb664d40f84c27bee45c193e4acb0fcd549b0305/rest/request.go#L1273)
554
- // calls FromObject (https://github.com/kubernetes/apimachinery/blob/7e441e0f246a2db6cf1855e4110892d1623a80cf/pkg/api/errors/errors.go#L100),
555
- // which generates a StatusError (https://github.com/kubernetes/apimachinery/blob/7e441e0f246a2db6cf1855e4110892d1623a80cf/pkg/api/errors/errors.go#L35) object.
556
- // *StatusError implements the Error interface using only the Message
557
- // field (https://github.com/kubernetes/apimachinery/blob/7e441e0f246a2db6cf1855e4110892d1623a80cf/pkg/api/errors/errors.go#L49)).
558
- // Therefore, when displaying the error, only the Message field will be available.
559
- resp := deny (metav1 .StatusReasonInvalid , msg )
560
- resp .Result .Details = & metav1.StatusDetails {
561
- Causes : []metav1.StatusCause {{
562
- Message : msg ,
563
- Field : field .String (),
564
- }},
565
- }
566
-
567
- return resp
568
- }
569
-
570
- // codeFromReason implements the needed subset of
571
- // https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#StatusReason
572
- func codeFromReason (reason metav1.StatusReason ) int32 {
573
- switch reason {
574
- case metav1 .StatusReasonUnknown :
575
- return 500
576
- case metav1 .StatusReasonUnauthorized :
577
- return 401
578
- case metav1 .StatusReasonForbidden :
579
- return 403
580
- case metav1 .StatusReasonConflict :
581
- return 409
582
- case metav1 .StatusReasonBadRequest :
583
- return 400
584
- case metav1 .StatusReasonInvalid :
585
- return 422
586
- case metav1 .StatusReasonInternalError :
587
- return 500
588
- case metav1 .StatusReasonServiceUnavailable :
589
- return 503
590
- default :
591
- return 500
592
- }
593
- }
0 commit comments