Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.

Commit 28fec50

Browse files
committed
Add MWH and update VWH on included-namespace label
Part of 13. Fixes 37. Add mutating webhook to set the correct `included-namespace` label on non-excluded namespaces if it's missing. Leave all other situation as is and let the validating webhook to do the validation. Update namespace validator to allow all other changes if the illegal included-namespace label is unchanged. Since namespace mutator ensures the label exists on included namespaces, update rules to ignore the case when the label is missing on the included namespaces. Add old instance field to the namespace validator. Decode it from request to compare with the new instance. Add more test cases since we now have more combination of cases to test. Update the namespace tree label update logs and function name in the HC reconciler to make it clear that the HC reconciler only updates the tree labels and won't update included-namespace label if the MWH works fine. Tested by `make test` with new test cases. Also tested manually by applying namespace manifests with different labels, viewed the logs and verified the MWH and VWH work fine.
1 parent ba15870 commit 28fec50

File tree

13 files changed

+363
-104
lines changed

13 files changed

+363
-104
lines changed

cmd/manager/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
v1a2 "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
4242
"sigs.k8s.io/hierarchical-namespaces/internal/config"
4343
"sigs.k8s.io/hierarchical-namespaces/internal/forest"
44+
"sigs.k8s.io/hierarchical-namespaces/internal/mutators"
4445
"sigs.k8s.io/hierarchical-namespaces/internal/reconcilers"
4546
"sigs.k8s.io/hierarchical-namespaces/internal/stats"
4647
"sigs.k8s.io/hierarchical-namespaces/internal/validators"
@@ -213,6 +214,10 @@ func startControllers(mgr ctrl.Manager, certsCreated chan struct{}) {
213214
validators.Create(mgr, f)
214215
}
215216

217+
// Create mutating admission controllers.
218+
setupLog.Info("Registering mutating webhook")
219+
mutators.Create(mgr)
220+
216221
// Create all reconciling controllers
217222
setupLog.Info("Creating controllers", "maxReconciles", maxReconciles)
218223
if err := reconcilers.Create(mgr, f, maxReconciles, false); err != nil {

config/webhook/manifests.yaml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
11

2+
---
3+
apiVersion: admissionregistration.k8s.io/v1
4+
kind: MutatingWebhookConfiguration
5+
metadata:
6+
creationTimestamp: null
7+
name: mutating-webhook-configuration
8+
webhooks:
9+
- admissionReviewVersions:
10+
- v1
11+
clientConfig:
12+
service:
13+
name: webhook-service
14+
namespace: system
15+
path: /mutate-namespace
16+
failurePolicy: Ignore
17+
name: namespacelabel.hnc.x-k8s.io
18+
rules:
19+
- apiGroups:
20+
- ""
21+
apiVersions:
22+
- v1
23+
operations:
24+
- CREATE
25+
- UPDATE
26+
resources:
27+
- namespaces
28+
sideEffects: None
29+
230
---
331
apiVersion: admissionregistration.k8s.io/v1
432
kind: ValidatingWebhookConfiguration
@@ -8,7 +36,6 @@ metadata:
836
webhooks:
937
- admissionReviewVersions:
1038
- v1
11-
- v1beta1
1239
clientConfig:
1340
service:
1441
name: webhook-service
@@ -29,7 +56,6 @@ webhooks:
2956
sideEffects: None
3057
- admissionReviewVersions:
3158
- v1
32-
- v1beta1
3359
clientConfig:
3460
service:
3561
name: webhook-service
@@ -50,7 +76,6 @@ webhooks:
5076
sideEffects: None
5177
- admissionReviewVersions:
5278
- v1
53-
- v1beta1
5479
clientConfig:
5580
service:
5681
name: webhook-service
@@ -72,7 +97,6 @@ webhooks:
7297
sideEffects: None
7398
- admissionReviewVersions:
7499
- v1
75-
- v1beta1
76100
clientConfig:
77101
service:
78102
name: webhook-service
@@ -94,7 +118,6 @@ webhooks:
94118
sideEffects: None
95119
- admissionReviewVersions:
96120
- v1
97-
- v1beta1
98121
clientConfig:
99122
service:
100123
name: webhook-service

internal/mutators/namespace.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package mutators
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
8+
"github.com/go-logr/logr"
9+
corev1 "k8s.io/api/core/v1"
10+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
11+
12+
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
13+
"sigs.k8s.io/hierarchical-namespaces/internal/config"
14+
)
15+
16+
// NamespaceIncludedLabelServingPath is where the mutator will run. Must be kept
17+
// in sync with the kubebuilder markers below.
18+
const (
19+
NamespaceIncludedLabelServingPath = "/mutate-namespace"
20+
)
21+
22+
// Note: the mutating webhook FAILS OPEN. This means that if the webhook goes
23+
// down, all further changes are allowed. (An empty line has to be kept below
24+
// the kubebuilder marker for the controller-gen to generate manifests.)
25+
//
26+
// +kubebuilder:webhook:admissionReviewVersions=v1,path=/mutate-namespace,mutating=true,failurePolicy=ignore,groups="",resources=namespaces,sideEffects=None,verbs=create;update,versions=v1,name=namespacelabel.hnc.x-k8s.io
27+
28+
type NamespaceIncludedLabel struct {
29+
Log logr.Logger
30+
decoder *admission.Decoder
31+
}
32+
33+
// Handle implements the mutating webhook.
34+
func (n *NamespaceIncludedLabel) Handle(ctx context.Context, req admission.Request) admission.Response {
35+
log := n.Log.WithValues("namespace", req.Name)
36+
ns := &corev1.Namespace{}
37+
err := n.decoder.Decode(req, ns)
38+
if err != nil {
39+
return admission.Errored(http.StatusBadRequest, err)
40+
}
41+
42+
n.handle(log, ns)
43+
marshaledNS, err := json.Marshal(ns)
44+
if err != nil {
45+
return admission.Errored(http.StatusInternalServerError, err)
46+
}
47+
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledNS)
48+
}
49+
50+
// handle implements the non-boilerplate logic of this mutator, allowing it to
51+
// be more easily unit tested (ie without constructing a full admission.Request).
52+
// Currently, we only add `included-namespace` label to non-excluded namespaces
53+
// if the label is missing.
54+
func (n *NamespaceIncludedLabel) handle(log logr.Logger, ns *corev1.Namespace) {
55+
// Early exit if the namespace is excluded.
56+
if config.ExcludedNamespaces[ns.Name] {
57+
return
58+
}
59+
60+
// Add label if the namespace doesn't have it.
61+
if _, hasLabel := ns.Labels[api.LabelIncludedNamespace]; !hasLabel {
62+
if ns.Labels == nil {
63+
ns.Labels = map[string]string{}
64+
}
65+
log.Info("Not an excluded namespace; set included-namespace label.")
66+
ns.Labels[api.LabelIncludedNamespace] = "true"
67+
}
68+
}
69+
70+
// InjectDecoder injects the decoder.
71+
func (n *NamespaceIncludedLabel) InjectDecoder(d *admission.Decoder) error {
72+
n.decoder = d
73+
return nil
74+
}

internal/mutators/namespace_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package mutators
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
corev1 "k8s.io/api/core/v1"
8+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
9+
10+
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
11+
"sigs.k8s.io/hierarchical-namespaces/internal/config"
12+
)
13+
14+
func TestMutateNamespaceIncludedLabel(t *testing.T) {
15+
m := &NamespaceIncludedLabel{}
16+
l := zap.New()
17+
config.ExcludedNamespaces = map[string]bool{"excluded": true}
18+
19+
tests := []struct {
20+
name string
21+
nsn string
22+
lval string
23+
expectlval string
24+
}{
25+
{name: "Set label on non-excluded namespace without label", nsn: "a", expectlval: "true"},
26+
{name: "No operation on non-excluded namespace with label (e.g. from a yaml file)", nsn: "a", lval: "true", expectlval: "true"},
27+
{name: "No operation on non-excluded namespace with label with wrong value(e.g. from a yaml file)", nsn: "a", lval: "wrong", expectlval: "wrong"},
28+
{name: "No operation on excluded namespace without label", nsn: "excluded"},
29+
{name: "No operation on excluded namespace with label (e.g. from a yaml file)", nsn: "excluded", lval: "true", expectlval: "true"},
30+
{name: "No operation on excluded namespace with label with wrong value (e.g. from a yaml file)", nsn: "excluded", lval: "wrong", expectlval: "wrong"},
31+
}
32+
for _, tc := range tests {
33+
t.Run(tc.name, func(t *testing.T) {
34+
g := NewWithT(t)
35+
36+
nsInst := &corev1.Namespace{}
37+
nsInst.Name = tc.nsn
38+
if tc.lval != "" {
39+
nsInst.SetLabels(map[string]string{api.LabelIncludedNamespace: tc.lval})
40+
}
41+
42+
// Test
43+
m.handle(l, nsInst)
44+
45+
// Report
46+
g.Expect(nsInst.Labels[api.LabelIncludedNamespace]).Should(Equal(tc.expectlval))
47+
})
48+
}
49+
}

internal/mutators/setup.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package mutators
2+
3+
import (
4+
ctrl "sigs.k8s.io/controller-runtime"
5+
"sigs.k8s.io/controller-runtime/pkg/webhook"
6+
)
7+
8+
// Create creates the mutator. This function is called from main.go.
9+
func Create(mgr ctrl.Manager) {
10+
// Create mutator for namespace `included-namespace` label.
11+
mgr.GetWebhookServer().Register(NamespaceIncludedLabelServingPath, &webhook.Admission{Handler: &NamespaceIncludedLabel{
12+
Log: ctrl.Log.WithName("mutators").WithName("NamespaceIncludedLabel"),
13+
}})
14+
}

internal/reconcilers/hierarchy_config.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func (r *HierarchyConfigReconciler) syncWithForest(log logr.Logger, nsInst *core
298298
r.syncConditions(log, inst, ns, deletingCRD, hadCrit)
299299

300300
// Sync the tree labels. This should go last since it can depend on the conditions.
301-
nsCustomerLabelUpdated := r.syncLabel(log, nsInst, ns)
301+
nsCustomerLabelUpdated := r.syncTreeLabels(log, nsInst, ns)
302302

303303
return initial || nsCustomerLabelUpdated
304304
}
@@ -461,8 +461,8 @@ func (r *HierarchyConfigReconciler) syncAnchors(log logr.Logger, ns *forest.Name
461461
}
462462
}
463463

464-
// Sync namespace tree labels and other labels. Return true if the labels are updated.
465-
func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
464+
// Sync namespace tree labels. Return true if the labels are updated.
465+
func (r *HierarchyConfigReconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
466466
if ns.IsExternal() {
467467
metadata.SetLabel(nsInst, nsInst.Name+api.LabelTreeDepthSuffix, "0")
468468
return false
@@ -504,7 +504,7 @@ func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Na
504504
// Update the labels in the forest so that we can quickly access the labels and
505505
// compare if they match the given selector
506506
if ns.SetLabels(nsInst.Labels) {
507-
log.Info("Namespace labels have been updated.")
507+
log.Info("Namespace tree labels have been updated.")
508508
return true
509509
}
510510
return false

internal/validators/anchor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const (
2323
// Note: the validating webhook FAILS CLOSE. This means that if the webhook goes down, all further
2424
// changes are forbidden.
2525
//
26-
// +kubebuilder:webhook:admissionReviewVersions=v1;v1beta1,path=/validate-hnc-x-k8s-io-v1alpha2-subnamespaceanchors,mutating=false,failurePolicy=fail,groups="hnc.x-k8s.io",resources=subnamespaceanchors,sideEffects=None,verbs=create;delete,versions=v1alpha2,name=subnamespaceanchors.hnc.x-k8s.io
26+
// +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;delete,versions=v1alpha2,name=subnamespaceanchors.hnc.x-k8s.io
2727

2828
type Anchor struct {
2929
Log logr.Logger

internal/validators/hierarchy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const (
3434
// changes to the hierarchy are forbidden. However, new objects will still be propagated according
3535
// to the existing hierarchy (unless the reconciler is down too).
3636
//
37-
// +kubebuilder:webhook:admissionReviewVersions=v1;v1beta1,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
37+
// +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
3838

3939
type Hierarchy struct {
4040
Log logr.Logger

internal/validators/hncconfig.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const (
2828
// Note: the validating webhook FAILS CLOSE. This means that if the webhook goes down, all further
2929
// changes are denied.
3030
//
31-
// +kubebuilder:webhook:admissionReviewVersions=v1;v1beta1,path=/validate-hnc-x-k8s-io-v1alpha2-hncconfigurations,mutating=false,failurePolicy=fail,groups="hnc.x-k8s.io",resources=hncconfigurations,sideEffects=None,verbs=create;update;delete,versions=v1alpha2,name=hncconfigurations.hnc.x-k8s.io
31+
// +kubebuilder:webhook:admissionReviewVersions=v1,path=/validate-hnc-x-k8s-io-v1alpha2-hncconfigurations,mutating=false,failurePolicy=fail,groups="hnc.x-k8s.io",resources=hncconfigurations,sideEffects=None,verbs=create;update;delete,versions=v1alpha2,name=hncconfigurations.hnc.x-k8s.io
3232

3333
type HNCConfig struct {
3434
Log logr.Logger

0 commit comments

Comments
 (0)