Skip to content

Commit 00f0634

Browse files
committed
Replace hierarchyconfig webhook with WebhookManagedBy.
1 parent a7a2923 commit 00f0634

File tree

4 files changed

+131
-165
lines changed

4 files changed

+131
-165
lines changed

internal/hierarchyconfig/validator.go

Lines changed: 72 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4040
type 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

Comments
 (0)