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

Commit ba15870

Browse files
authored
Merge pull request #36 from yiqigao217/include
Replace excluded namespace label with included
2 parents 467d883 + d11d38e commit ba15870

File tree

8 files changed

+171
-118
lines changed

8 files changed

+171
-118
lines changed

Makefile

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -180,23 +180,11 @@ controller-gen:
180180

181181
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config.
182182
#
183-
# We only delete the deployment and the validatingwebhookconfiguration if they
184-
# exist before applying the manifest, because
183+
# We only delete the deployment if it exists before applying the manifest, because
185184
# a) deleting the CRDs will cause all the existing CRs to be wiped away;
186-
# b) if not deleting the deployment, a new image won't be pulled unless the tag changes;
187-
# c) if not deleting the validatingwebhookconfiguration, we cannot label
188-
# namespaces to exclude them if the HNC pod is already in a bad state.
189-
#
190-
# Then we ensure the system namespaces are excluded before we deploy HNC. This
191-
# step is critical because if the HNC pod is ever in a bad state, the object
192-
# webhook service would not respond and would stop everything in the system
193-
# namespaces, such as "kube-system", thus breaking the whole cluster.
185+
# b) if not deleting the deployment, a new image won't be pulled unless the tag changes.
194186
deploy: docker-push kubectl manifests
195187
-kubectl -n hnc-system delete deployment hnc-controller-manager
196-
-kubectl delete validatingwebhookconfiguration hnc-validating-webhook-configuration
197-
-kubectl label ns kube-node-lease hnc.x-k8s.io/excluded-namespace=true --overwrite
198-
-kubectl label ns kube-public hnc.x-k8s.io/excluded-namespace=true --overwrite
199-
-kubectl label ns kube-system hnc.x-k8s.io/excluded-namespace=true --overwrite
200188
kubectl apply -f manifests/${HNC_IMG_NAME}.yaml
201189

202190
deploy-watch:
@@ -263,7 +251,7 @@ kind-deploy:
263251
# HNC_FOCUS=772 make test-e2e # only runs the test for issue 772
264252
# HNC_FOCUS=Quickstart make test-e2e # Runs all tests in the "Quickstart" Describe block
265253
.PHONY: test-e2e
266-
test-e2e: warn-hnc-repair exclude-system-namespaces
254+
test-e2e: warn-hnc-repair
267255
go clean -testcache
268256
ifndef HNC_FOCUS
269257
go test -v -timeout 0 ./test/e2e/...
@@ -274,7 +262,7 @@ endif
274262
# This batch test will run e2e tests N times on the current cluster the user
275263
# deployed (either kind or a kubernetes cluster), e.g. "make test-e2e-batch N=10"
276264
.PHONY: test-e2e-batch
277-
test-e2e-batch: warn-hnc-repair exclude-system-namespaces
265+
test-e2e-batch: warn-hnc-repair
278266
number=1 ; while [[ $$number -le $N ]] ; do \
279267
echo $$number ; \
280268
((number = number + 1)) ; \
@@ -285,27 +273,10 @@ test-e2e-batch: warn-hnc-repair exclude-system-namespaces
285273
# This will *only* run a small number of tests (specifically, the quickstarts). You can run this when you're fairly confident that
286274
# everything will work, but be sure to watch for the postsubmits and periodic tests to ensure they pass too.
287275
.PHONY: test-smoke
288-
test-smoke: exclude-system-namespaces
276+
test-smoke:
289277
go clean --testcache
290278
go test -v -timeout 0 ./test/e2e/... -args --ginkgo.focus "Quickstart"
291279

292-
# exclude-system-namespaces is called before we run any e2e tests. We do ensure
293-
# the system namespaces are excluded in the "deploy" target. However, we need to
294-
# do it here too in case users install HNC by applying manifests. Ensuring the
295-
# system namespaces excluded is critical, because otherwise when HNC pod is in a
296-
# bad state, the whole cluster will break.
297-
exclude-system-namespaces:
298-
@echo
299-
@echo "Ensuring all system namespaces are excluded from HNC..."
300-
@kubectl label ns hnc-system hnc.x-k8s.io/excluded-namespace=true --overwrite
301-
@kubectl label ns kube-node-lease hnc.x-k8s.io/excluded-namespace=true --overwrite
302-
@kubectl label ns kube-public hnc.x-k8s.io/excluded-namespace=true --overwrite
303-
@kubectl label ns kube-system hnc.x-k8s.io/excluded-namespace=true --overwrite
304-
@echo
305-
@echo "If these tests fail due to the webhook not being ready, wait 30s and try again. Note that webhooks can take up to 30s to become ready"
306-
@echo "after HNC is first deployed in a cluster."
307-
@echo
308-
309280
warn-hnc-repair:
310281
@echo "************************************************************"
311282
ifndef HNC_REPAIR

api/v1alpha2/hierarchy_types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ const (
4444
// about this standard label when we invented our own).
4545
LabelManagedByApps = "app.kubernetes.io/managed-by"
4646

47-
// LabelExcludedNamespace is the label added by users on the namespaces that
48-
// should be excluded from our validators, e.g. "kube-system".
49-
LabelExcludedNamespace = MetaGroup + "/excluded-namespace"
47+
// LabelIncludedNamespace is the label added by HNC on the namespaces that
48+
// should be enforced by our validators.
49+
LabelIncludedNamespace = MetaGroup + "/included-namespace"
5050
)
5151

5252
const (

config/manager/manager.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,6 @@ kind: Namespace
33
metadata:
44
labels:
55
control-plane: controller-manager
6-
# Add excluded namespace label on `hnc-system` namespace by default because
7-
# without this label when installing HNC, there will be a deadlock that
8-
# the object webhook fails close so the cert-rotator cannot create/update
9-
# `hnc-webhook-server-cert` secret object for VWHConfiguration, thus the
10-
# webhooks will never be ready.
11-
hnc.x-k8s.io/excluded-namespace: "true"
126
name: system
137
---
148
apiVersion: apps/v1

config/webhook/webhook_patch.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ webhooks:
77
- name: objects.hnc.x-k8s.io
88
timeoutSeconds: 2
99
sideEffects: None
10-
# We only filter out excluded namespaces from this object validator so that
11-
# when HNC (webhook service specifically) is down, operations in the excluded
10+
# We only apply this object validator on non-excluded namespaces, which have
11+
# the "included-namespace" label set by the HC reconciler, so that when HNC
12+
# (webhook service specifically) is down, operations in the excluded
1213
# namespaces won't be affected. Validators on HNC CRs are not filtered because
1314
# they are supposed to prevent abuse of HNC CRs in excluded namespaces.
14-
# Namespace validator is not filtered to prevent abuse of excluded namespace.
15-
# Unfortunately, this means that when HNC is down, we will block updates on all
16-
# namespaces, even "excluded" ones, but anyone who can update namespaces like
17-
# `kube-system` should likely be able to delete the VWHConfiguration to make
18-
# the updates.
15+
# Namespace validator is not filtered to prevent abuse of the included-namespace
16+
# label on excluded namespaces. Unfortunately, this means that when HNC is
17+
# down, we will block updates on all namespaces, even "excluded" ones, but
18+
# anyone who can update namespaces like `kube-system` should likely be able to
19+
# delete the VWHConfiguration to make the updates.
1920
namespaceSelector:
20-
matchExpressions:
21-
- key: hnc.x-k8s.io/excluded-namespace
22-
operator: DoesNotExist
21+
matchLabels:
22+
hnc.x-k8s.io/included-namespace: "true"
2323
rules:
2424
# This overwrites the rules specified in the object validator to patch object
2525
# scope of `namespaced` since there's no kubebuilder marker for `scope`.

internal/reconcilers/hierarchy_config.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,9 @@ func (r *HierarchyConfigReconciler) Reconcile(ctx context.Context, req ctrl.Requ
8888
ns := req.NamespacedName.Namespace
8989
log := loggerWithRID(r.Log).WithValues("ns", ns)
9090

91-
// Always delete hierarchyconfiguration (and any other HNC CRs) in the
92-
// excluded namespaces and early exit.
91+
// Early exit if it's an excluded namespace.
9392
if config.ExcludedNamespaces[ns] {
94-
// Since singletons in the excluded namespaces are never synced by HNC, there
95-
// are no finalizers on the singletons that we can delete them without
96-
// removing the finalizers first.
97-
return ctrl.Result{}, r.deleteSingletonIfExists(ctx, log, ns)
93+
return ctrl.Result{}, r.handleExcludedNamespace(ctx, log, ns)
9894
}
9995

10096
stats.StartHierConfigReconcile()
@@ -103,6 +99,35 @@ func (r *HierarchyConfigReconciler) Reconcile(ctx context.Context, req ctrl.Requ
10399
return ctrl.Result{}, r.reconcile(ctx, log, ns)
104100
}
105101

102+
func (r *HierarchyConfigReconciler) handleExcludedNamespace(ctx context.Context, log logr.Logger, nm string) error {
103+
// Get the namespace. Early exist if the namespace doesn't exist or is purged.
104+
// If so, there must be no namespace label or HC instance to delete.
105+
nsInst, err := r.getNamespace(ctx, nm)
106+
if err != nil {
107+
if apierrors.IsNotFound(err) {
108+
return nil
109+
}
110+
return err
111+
}
112+
113+
// Remove the "included-namespace" label on excluded namespace if it exists.
114+
origNS := nsInst.DeepCopy()
115+
r.removeIncludedNamespaceLabel(log, nsInst)
116+
if _, err := r.writeNamespace(ctx, log, origNS, nsInst); err != nil {
117+
return err
118+
}
119+
120+
// Always delete hierarchyconfiguration (and any other HNC CRs) in the
121+
// excluded namespaces. Note: since singletons in the excluded namespaces are
122+
// never synced by HNC, there are no finalizers on the singletons that we can
123+
// delete them without removing the finalizers first.
124+
if err := r.deleteSingletonIfExists(ctx, log, nm); err != nil {
125+
return err
126+
}
127+
128+
return nil
129+
}
130+
106131
func (r *HierarchyConfigReconciler) reconcile(ctx context.Context, log logr.Logger, nm string) error {
107132
// Load the namespace and make a copy
108133
nsInst, err := r.getNamespace(ctx, nm)
@@ -117,9 +142,9 @@ func (r *HierarchyConfigReconciler) reconcile(ctx context.Context, log logr.Logg
117142
}
118143
origNS := nsInst.DeepCopy()
119144

120-
// Remove excluded namespace label if there's one on the namespace since
121-
// the excluded namespaces should be already skipped earlier.
122-
r.removeExcludedNamespaceLabel(log, nsInst)
145+
// Add the "included-namespace" label to the namespace if it doesn't exist. The
146+
// excluded namespaces should be already skipped earlier.
147+
r.addIncludedNamespaceLabel(log, nsInst)
123148

124149
// Get singleton from apiserver. If it doesn't exist, initialize one.
125150
inst, deletingCRD, err := r.getSingleton(ctx, nm)
@@ -178,14 +203,29 @@ func (r *HierarchyConfigReconciler) onMissingNamespace(log logr.Logger, nm strin
178203
}
179204
}
180205

181-
// removeExcludedNamespaceLabel removes the `hnc.x-k8s.io/excluded-namespace`
206+
// removeIncludedNamespaceLabel removes the `hnc.x-k8s.io/included-namespace`
182207
// label from the namespace.
183-
func (r *HierarchyConfigReconciler) removeExcludedNamespaceLabel(log logr.Logger, nsInst *corev1.Namespace) {
208+
func (r *HierarchyConfigReconciler) removeIncludedNamespaceLabel(log logr.Logger, nsInst *corev1.Namespace) {
184209
lbs := nsInst.Labels
185-
if _, found := lbs[api.LabelExcludedNamespace]; found {
186-
log.Info("Illegal excluded-namespace label found; removing")
187-
delete(lbs, api.LabelExcludedNamespace)
210+
if _, found := lbs[api.LabelIncludedNamespace]; found {
211+
log.Info("Illegal included-namespace label found; removing")
212+
delete(lbs, api.LabelIncludedNamespace)
213+
nsInst.SetLabels(lbs)
214+
}
215+
}
216+
217+
// addIncludedNamespaceLabel adds the `hnc.x-k8s.io/included-namespace` label to the
218+
// namespace.
219+
func (r *HierarchyConfigReconciler) addIncludedNamespaceLabel(log logr.Logger, nsInst *corev1.Namespace) {
220+
lbs := nsInst.Labels
221+
if lbs == nil {
222+
lbs = make(map[string]string)
223+
}
224+
if v, found := lbs[api.LabelIncludedNamespace]; found && v == "true" {
225+
return
188226
}
227+
log.Info("Adding included-namespace label")
228+
lbs[api.LabelIncludedNamespace] = "true"
189229
nsInst.SetLabels(lbs)
190230
}
191231

internal/reconcilers/hierarchy_config_test.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -381,26 +381,31 @@ var _ = Describe("Hierarchy", func() {
381381
Eventually(getLabel(ctx, nms[5], nms[0]+api.LabelTreeDepthSuffix)).Should(Equal("3"))
382382
})
383383

384-
It("should remove excluded namespace labels from non-excluded namespaces", func() {
384+
It("should remove included-namespace namespace labels from excluded namespaces", func() {
385385
config.ExcludedNamespaces = map[string]bool{"kube-system": true}
386-
l := map[string]string{api.LabelExcludedNamespace: "true"}
387-
388-
// Set excluded namespace labels on foo and bar. We are not verifying the
389-
// labels are added here because our reconciler will remove it and the
390-
// verification would be flaky.
391-
fooName := createNSWithLabel(ctx, "foo", l)
392-
barName := createNSWithLabel(ctx, "bar", l)
393-
394386
kubeSystem := getNamespace(ctx, "kube-system")
387+
388+
// Add additional label "other:other" to verify the labels are updated.
389+
l := map[string]string{api.LabelIncludedNamespace: "true", "other": "other"}
395390
kubeSystem.SetLabels(l)
396391
updateNamespace(ctx, kubeSystem)
397-
Eventually(getLabel(ctx, "kube-system", api.LabelExcludedNamespace)).Should(Equal("true"))
392+
// Verify the labels are updated on the namespace.
393+
Eventually(getLabel(ctx, "kube-system", "other")).Should(Equal("other"))
394+
// Verify the included-namespace label is removed by the HC reconciler.
395+
Eventually(getLabel(ctx, "kube-system", api.LabelIncludedNamespace)).Should(Equal(""))
396+
})
397+
398+
It("should set included-namespace namespace labels properly on non-excluded namespaces", func() {
399+
// Create a namespace without any labels.
400+
fooName := createNS(ctx, "foo")
401+
// Verify the label is eventually added by the HC reconciler.
402+
Eventually(getLabel(ctx, fooName, api.LabelIncludedNamespace)).Should(Equal("true"))
398403

399-
// Verify the excluded namespace label is removed from both foo and bar, but
400-
// is not removed from kube-system.
401-
Eventually(getLabel(ctx, fooName, api.LabelExcludedNamespace)).Should(Equal(""))
402-
Eventually(getLabel(ctx, barName, api.LabelExcludedNamespace)).Should(Equal(""))
403-
Eventually(getLabel(ctx, "kube-system", api.LabelExcludedNamespace)).Should(Equal("true"))
404+
l := map[string]string{api.LabelIncludedNamespace: "false"}
405+
// Create a namespace with the label with a wrong value.
406+
barName := createNSWithLabel(ctx, "bar", l)
407+
// Verify the label is eventually updated to have the right value.
408+
Eventually(getLabel(ctx, barName, api.LabelIncludedNamespace)).Should(Equal("true"))
404409
})
405410
})
406411

internal/validators/namespace.go

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (v *Namespace) handle(req *nsRequest) admission.Response {
7676

7777
switch req.op {
7878
case k8sadm.Create:
79-
if rsp := v.illegalExcludedNamespaceLabel(req); !rsp.Allowed {
79+
if rsp := v.illegalIncludedNamespaceLabel(req); !rsp.Allowed {
8080
return rsp
8181
}
8282
// This check only applies to the Create operation since namespace name
@@ -85,7 +85,7 @@ func (v *Namespace) handle(req *nsRequest) admission.Response {
8585
return rsp
8686
}
8787
case k8sadm.Update:
88-
if rsp := v.illegalExcludedNamespaceLabel(req); !rsp.Allowed {
88+
if rsp := v.illegalIncludedNamespaceLabel(req); !rsp.Allowed {
8989
return rsp
9090
}
9191
// This check only applies to the Update operation. Creating a namespace
@@ -106,20 +106,46 @@ func (v *Namespace) handle(req *nsRequest) admission.Response {
106106
return allow("")
107107
}
108108

109-
func (v *Namespace) illegalExcludedNamespaceLabel(req *nsRequest) admission.Response {
110-
for l := range req.ns.Labels {
111-
if l == api.LabelExcludedNamespace && !config.ExcludedNamespaces[req.ns.Name] {
112-
// Note: this only blocks the request if it has a newly added illegal
113-
// excluded-namespace label because existing illegal excluded-namespace
114-
// label should have already been removed by our reconciler. For example,
115-
// even when the VWHConfiguration is removed, adding the label to a non-
116-
// excluded namespace would pass but the label is immediately removed; when
117-
// the VWHConfiguration is there but the reconcilers are down, any request
118-
// gets denied anyway.
119-
msg := fmt.Sprintf("You cannot exclude this namespace using the %q label. See https://github.com/kubernetes-sigs/hierarchical-namespaces/blob/master/docs/user-guide/concepts.md#excluded-namespace-label for detail.", api.LabelExcludedNamespace)
120-
return deny(metav1.StatusReasonForbidden, msg)
121-
}
109+
// illegalIncludedNamespaceLabel checks if there's any illegal use of the
110+
// included-namespace label on namespaces.
111+
// TODO these validating rules will be removed and converted into a mutating
112+
// webhook. See https://github.com/kubernetes-sigs/hierarchical-namespaces/issues/37
113+
func (v *Namespace) illegalIncludedNamespaceLabel(req *nsRequest) admission.Response {
114+
labelValue, hasLabel := req.ns.Labels[api.LabelIncludedNamespace]
115+
isExcluded := config.ExcludedNamespaces[req.ns.Name]
116+
117+
// An excluded namespace should not have the included-namespace label.
118+
//
119+
// Note: this only blocks the request if the excluded namespace is newly
120+
// created or is updated with a newly added included-namespace label because
121+
// existing illegal included-namespace label should have already been removed
122+
// by our reconciler. For example, even when the VWHConfiguration is removed,
123+
// adding the label to an excluded namespace would pass but the label is
124+
// immediately removed; when the VWHConfiguration is there but the reconcilers
125+
// are down, any request gets denied anyway.
126+
if isExcluded && hasLabel {
127+
// Todo add label concept and add `See https://github.com/kubernetes-sigs/hierarchical-namespaces/blob/master/docs/user-guide/concepts.md#included-namespace-label for detail`
128+
// to the message below. Github issue - https://github.com/kubernetes-sigs/hierarchical-namespaces/issues/9
129+
msg := fmt.Sprintf("You cannot enforce webhook rules on this excluded namespace using the %q label.", api.LabelIncludedNamespace)
130+
return deny(metav1.StatusReasonForbidden, msg)
122131
}
132+
133+
// An included-namespace should not have the included-namespace label with a
134+
// value other than "true" at any time. We will allow updating an included
135+
// namespace by removing its included-namespace label, since people or system
136+
// may apply namespace manifests (without the label) automatically and it
137+
// doesn't make sense to block their attempts after the first time.
138+
//
139+
// Note: this only blocks the request if the included namespace is just
140+
// created or updated with a wrong value for the included-namespace label,
141+
// because our reconciler should already set it correctly before the request.
142+
if !isExcluded && hasLabel && labelValue != "true" {
143+
// Todo add label concept and add `See https://github.com/kubernetes-sigs/hierarchical-namespaces/blob/master/docs/user-guide/concepts.md#included-namespace-label for detail`
144+
// to the message below. Github issue - https://github.com/kubernetes-sigs/hierarchical-namespaces/issues/9
145+
msg := fmt.Sprintf("You cannot unset the value of the %q label. Only HNC can edit this label on namespaces.", api.LabelIncludedNamespace)
146+
return deny(metav1.StatusReasonForbidden, msg)
147+
}
148+
123149
return allow("")
124150
}
125151

0 commit comments

Comments
 (0)