Skip to content

Commit

Permalink
Add MWH and update VWH on included-namespace label
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yiqigao217 committed May 19, 2021
1 parent ba15870 commit 28fec50
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 104 deletions.
5 changes: 5 additions & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
v1a2 "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
"sigs.k8s.io/hierarchical-namespaces/internal/config"
"sigs.k8s.io/hierarchical-namespaces/internal/forest"
"sigs.k8s.io/hierarchical-namespaces/internal/mutators"
"sigs.k8s.io/hierarchical-namespaces/internal/reconcilers"
"sigs.k8s.io/hierarchical-namespaces/internal/stats"
"sigs.k8s.io/hierarchical-namespaces/internal/validators"
Expand Down Expand Up @@ -213,6 +214,10 @@ func startControllers(mgr ctrl.Manager, certsCreated chan struct{}) {
validators.Create(mgr, f)
}

// Create mutating admission controllers.
setupLog.Info("Registering mutating webhook")
mutators.Create(mgr)

// Create all reconciling controllers
setupLog.Info("Creating controllers", "maxReconciles", maxReconciles)
if err := reconcilers.Create(mgr, f, maxReconciles, false); err != nil {
Expand Down
33 changes: 28 additions & 5 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@

---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
creationTimestamp: null
name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-namespace
failurePolicy: Ignore
name: namespacelabel.hnc.x-k8s.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- namespaces
sideEffects: None

---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
Expand All @@ -8,7 +36,6 @@ metadata:
webhooks:
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
Expand All @@ -29,7 +56,6 @@ webhooks:
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
Expand All @@ -50,7 +76,6 @@ webhooks:
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
Expand All @@ -72,7 +97,6 @@ webhooks:
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
Expand All @@ -94,7 +118,6 @@ webhooks:
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
Expand Down
74 changes: 74 additions & 0 deletions internal/mutators/namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package mutators

import (
"context"
"encoding/json"
"net/http"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
"sigs.k8s.io/hierarchical-namespaces/internal/config"
)

// NamespaceIncludedLabelServingPath is where the mutator will run. Must be kept
// in sync with the kubebuilder markers below.
const (
NamespaceIncludedLabelServingPath = "/mutate-namespace"
)

// Note: the mutating webhook FAILS OPEN. This means that if the webhook goes
// down, all further changes are allowed. (An empty line has to be kept below
// the kubebuilder marker for the controller-gen to generate manifests.)
//
// +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

type NamespaceIncludedLabel struct {
Log logr.Logger
decoder *admission.Decoder
}

// Handle implements the mutating webhook.
func (n *NamespaceIncludedLabel) Handle(ctx context.Context, req admission.Request) admission.Response {
log := n.Log.WithValues("namespace", req.Name)
ns := &corev1.Namespace{}
err := n.decoder.Decode(req, ns)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

n.handle(log, ns)
marshaledNS, err := json.Marshal(ns)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledNS)
}

// handle implements the non-boilerplate logic of this mutator, allowing it to
// be more easily unit tested (ie without constructing a full admission.Request).
// Currently, we only add `included-namespace` label to non-excluded namespaces
// if the label is missing.
func (n *NamespaceIncludedLabel) handle(log logr.Logger, ns *corev1.Namespace) {
// Early exit if the namespace is excluded.
if config.ExcludedNamespaces[ns.Name] {
return
}

// Add label if the namespace doesn't have it.
if _, hasLabel := ns.Labels[api.LabelIncludedNamespace]; !hasLabel {
if ns.Labels == nil {
ns.Labels = map[string]string{}
}
log.Info("Not an excluded namespace; set included-namespace label.")
ns.Labels[api.LabelIncludedNamespace] = "true"
}
}

// InjectDecoder injects the decoder.
func (n *NamespaceIncludedLabel) InjectDecoder(d *admission.Decoder) error {
n.decoder = d
return nil
}
49 changes: 49 additions & 0 deletions internal/mutators/namespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package mutators

import (
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
"sigs.k8s.io/hierarchical-namespaces/internal/config"
)

func TestMutateNamespaceIncludedLabel(t *testing.T) {
m := &NamespaceIncludedLabel{}
l := zap.New()
config.ExcludedNamespaces = map[string]bool{"excluded": true}

tests := []struct {
name string
nsn string
lval string
expectlval string
}{
{name: "Set label on non-excluded namespace without label", nsn: "a", expectlval: "true"},
{name: "No operation on non-excluded namespace with label (e.g. from a yaml file)", nsn: "a", lval: "true", expectlval: "true"},
{name: "No operation on non-excluded namespace with label with wrong value(e.g. from a yaml file)", nsn: "a", lval: "wrong", expectlval: "wrong"},
{name: "No operation on excluded namespace without label", nsn: "excluded"},
{name: "No operation on excluded namespace with label (e.g. from a yaml file)", nsn: "excluded", lval: "true", expectlval: "true"},
{name: "No operation on excluded namespace with label with wrong value (e.g. from a yaml file)", nsn: "excluded", lval: "wrong", expectlval: "wrong"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

nsInst := &corev1.Namespace{}
nsInst.Name = tc.nsn
if tc.lval != "" {
nsInst.SetLabels(map[string]string{api.LabelIncludedNamespace: tc.lval})
}

// Test
m.handle(l, nsInst)

// Report
g.Expect(nsInst.Labels[api.LabelIncludedNamespace]).Should(Equal(tc.expectlval))
})
}
}
14 changes: 14 additions & 0 deletions internal/mutators/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mutators

import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// Create creates the mutator. This function is called from main.go.
func Create(mgr ctrl.Manager) {
// Create mutator for namespace `included-namespace` label.
mgr.GetWebhookServer().Register(NamespaceIncludedLabelServingPath, &webhook.Admission{Handler: &NamespaceIncludedLabel{
Log: ctrl.Log.WithName("mutators").WithName("NamespaceIncludedLabel"),
}})
}
8 changes: 4 additions & 4 deletions internal/reconcilers/hierarchy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func (r *HierarchyConfigReconciler) syncWithForest(log logr.Logger, nsInst *core
r.syncConditions(log, inst, ns, deletingCRD, hadCrit)

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

return initial || nsCustomerLabelUpdated
}
Expand Down Expand Up @@ -461,8 +461,8 @@ func (r *HierarchyConfigReconciler) syncAnchors(log logr.Logger, ns *forest.Name
}
}

// Sync namespace tree labels and other labels. Return true if the labels are updated.
func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
// Sync namespace tree labels. Return true if the labels are updated.
func (r *HierarchyConfigReconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
if ns.IsExternal() {
metadata.SetLabel(nsInst, nsInst.Name+api.LabelTreeDepthSuffix, "0")
return false
Expand Down Expand Up @@ -504,7 +504,7 @@ func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Na
// Update the labels in the forest so that we can quickly access the labels and
// compare if they match the given selector
if ns.SetLabels(nsInst.Labels) {
log.Info("Namespace labels have been updated.")
log.Info("Namespace tree labels have been updated.")
return true
}
return false
Expand Down
2 changes: 1 addition & 1 deletion internal/validators/anchor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
// Note: the validating webhook FAILS CLOSE. This means that if the webhook goes down, all further
// changes are forbidden.
//
// +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
// +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

type Anchor struct {
Log logr.Logger
Expand Down
2 changes: 1 addition & 1 deletion internal/validators/hierarchy.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const (
// changes to the hierarchy are forbidden. However, new objects will still be propagated according
// to the existing hierarchy (unless the reconciler is down too).
//
// +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
// +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

type Hierarchy struct {
Log logr.Logger
Expand Down
2 changes: 1 addition & 1 deletion internal/validators/hncconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (
// Note: the validating webhook FAILS CLOSE. This means that if the webhook goes down, all further
// changes are denied.
//
// +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
// +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

type HNCConfig struct {
Log logr.Logger
Expand Down
Loading

0 comments on commit 28fec50

Please sign in to comment.