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

Commit 5eb827d

Browse files
authored
Merge pull request #93 from adrianludwin/managed
Improve usability of unmanaged namespaces
2 parents 1b81cfb + 0855807 commit 5eb827d

13 files changed

+120
-114
lines changed

internal/anchor/reconciler.go

+17-11
Original file line numberDiff line numberDiff line change
@@ -75,23 +75,29 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
7575
return ctrl.Result{}, err
7676
}
7777

78-
// Always delete anchor (and any other HNC CRs) in the excluded namespaces and
79-
// early exit.
80-
if !config.IsNamespaceIncluded(pnm) {
81-
// Since the anchors in the excluded namespaces are never synced by HNC,
82-
// there are no finalizers on the anchors that we can delete them without
83-
// removing the finalizers first.
84-
log.Info("Deleting anchor in an excluded namespace")
85-
return ctrl.Result{}, r.deleteInstance(ctx, log, inst)
78+
// Anchors in unmanaged namespace should be ignored. Make sure it
79+
// doesn't have any finalizers, otherwise, leave it alone.
80+
if why := config.WhyUnmanaged(pnm); why != "" {
81+
if len(inst.ObjectMeta.Finalizers) > 0 {
82+
log.Info("Removing finalizers from anchor in unmanaged namespace", "reason", why)
83+
inst.ObjectMeta.Finalizers = nil
84+
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
85+
}
86+
return ctrl.Result{}, nil
8687
}
8788

8889
// Report "Forbidden" state and early exit if the anchor name is an excluded
8990
// namespace that should not be created as a subnamespace, but the webhook has
9091
// been bypassed and the anchor has been successfully created. Forbidden
9192
// anchors won't have finalizers.
92-
if !config.IsNamespaceIncluded(nm) {
93-
inst.Status.State = api.Forbidden
94-
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
93+
if why := config.WhyUnmanaged(nm); why != "" {
94+
if inst.Status.State != api.Forbidden || len(inst.ObjectMeta.Finalizers) > 0 {
95+
log.Info("Setting forbidden state on anchor with unmanaged name", "reason", why)
96+
inst.Status.State = api.Forbidden
97+
inst.ObjectMeta.Finalizers = nil
98+
return ctrl.Result{}, r.writeInstance(ctx, log, inst)
99+
}
100+
return ctrl.Result{}, nil
95101
}
96102

97103
// Get the subnamespace. If it doesn't exist, initialize one.

internal/anchor/validator.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,14 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {
7979

8080
switch req.op {
8181
case k8sadm.Create:
82-
// Can't create subnamespaces in excluded namespaces
83-
if !config.IsNamespaceIncluded(pnm) {
84-
msg := fmt.Sprintf("Cannot create a subnamespace in the excluded namespace %q", pnm)
82+
// Can't create subnamespaces in unmanaged namespaces
83+
if why := config.WhyUnmanaged(pnm); why != "" {
84+
msg := fmt.Sprintf("Cannot create a subnamespace in the unmanaged namespace %q (%s)", pnm, why)
8585
return webhooks.Deny(metav1.StatusReasonForbidden, msg)
8686
}
87-
// Can't create subnamespaces using excluded namespace names
88-
if !config.IsNamespaceIncluded(cnm) {
89-
msg := fmt.Sprintf("Cannot create a subnamespace using the excluded namespace name %q", cnm)
87+
// Can't create subnamespaces using unmanaged namespace names
88+
if why := config.WhyUnmanaged(cnm); why != "" {
89+
msg := fmt.Sprintf("Cannot create a subnamespace using the unmanaged namespace name %q (%s)", cnm, why)
9090
return webhooks.Deny(metav1.StatusReasonForbidden, msg)
9191
}
9292

internal/config/default_config.go

-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package config
22

3-
import "regexp"
4-
53
// UnpropgatedAnnotations is a list of annotations on objects that should _not_ be propagated by HNC.
64
// Much like HNC itself, other systems (such as GKE Config Sync) use annotations to "claim" an
75
// object - such as deleting objects it doesn't recognize. By removing these annotations on
@@ -10,11 +8,3 @@ import "regexp"
108
// This value is controlled by the --unpropagated-annotation command line, which may be set multiple
119
// times.
1210
var UnpropagatedAnnotations []string
13-
14-
// ExcludedNamespaces is a list of namespaces used by reconcilers and validators
15-
// to exclude namespaces that shouldn't be reconciled or validated.
16-
//
17-
// This value is controlled by the --excluded-namespace command line, which may
18-
// be set multiple times.
19-
var excludedNamespaces map[string]bool
20-
var includedNamespacesRegex *regexp.Regexp

internal/config/namespace.go

+41-6
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,62 @@ import (
44
"regexp"
55
)
66

7+
var (
8+
// excludedNamespaces is a list of namespaces used by reconcilers and validators
9+
// to exclude namespaces that shouldn't be reconciled or validated.
10+
//
11+
// This value is controlled by the --excluded-namespace command line, which may
12+
// be set multiple times.
13+
excludedNamespaces map[string]bool
14+
15+
// includedNamespacesRegex is the compiled regex of included namespaces
16+
includedNamespacesRegex *regexp.Regexp
17+
18+
// includedNamespacesStr is the original pattern of the regex. It must
19+
// only be used to generate error messages.
20+
includedNamespacesStr string
21+
)
22+
723
func SetNamespaces(regex string, excluded ...string) {
824
if regex == "" {
925
regex = ".*"
1026
}
1127

12-
includedNamespacesRegex = regexp.MustCompile("^" + regex + "$")
28+
includedNamespacesStr = "^" + regex + "$"
29+
includedNamespacesRegex = regexp.MustCompile(includedNamespacesStr)
1330

1431
excludedNamespaces = make(map[string]bool)
1532
for _, exn := range excluded {
1633
excludedNamespaces[exn] = true
1734
}
1835
}
1936

20-
func IsNamespaceIncluded(name string) bool {
21-
if excludedNamespaces[name] {
22-
return false
37+
// WhyUnamanged returns a human-readable message explaining why the given
38+
// namespace is unmanaged, or an empty string if it *is* managed.
39+
func WhyUnmanaged(nm string) string {
40+
// We occasionally check if the _parent_ of a namespace is managed.
41+
// It's an error for a managed namespace to have an unmanaged parent,
42+
// so we'll treat "no parent" as managed.
43+
if nm == "" {
44+
return ""
45+
}
46+
47+
if excludedNamespaces[nm] {
48+
return "excluded by the HNC administrator"
2349
}
2450

2551
if includedNamespacesRegex == nil { // unit tests
26-
return true
52+
return ""
53+
}
54+
55+
if !includedNamespacesRegex.MatchString(nm) {
56+
return "does not match the regex set by the HNC administrator: `" + includedNamespacesStr + "`"
2757
}
2858

29-
return includedNamespacesRegex.MatchString(name)
59+
return ""
60+
}
61+
62+
// IsManagedNamespace is the same as WhyUnmanaged but converts the response to a bool for convenience.
63+
func IsManagedNamespace(nm string) bool {
64+
return WhyUnmanaged(nm) == ""
3065
}

internal/config/namespace_test.go

+12-12
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,28 @@ import (
88
func TestIsNamespaceIncluded(t *testing.T) {
99

1010
tests := []struct {
11-
name string
12-
regex string
13-
excludeNamespaces []string
14-
expect bool
11+
name string
12+
regex string
13+
excluded []string
14+
expect bool
1515
}{
16-
{name: "foobar", regex: "foo.*", excludeNamespaces: []string{"bar"}, expect: true},
17-
{name: "bar", regex: "foo-.*", excludeNamespaces: []string{"bar"}, expect: false},
18-
{name: "bar", regex: ".*", excludeNamespaces: []string{"bar"}, expect: false},
19-
{name: "foo", regex: ".*", excludeNamespaces: []string{"bar"}, expect: true},
20-
{name: "foo", regex: ".*", excludeNamespaces: []string{"bar", "foo"}, expect: false},
16+
{name: "foobar", regex: "foo.*", excluded: []string{"bar"}, expect: true},
17+
{name: "bar", regex: "foo-.*", excluded: []string{"bar"}, expect: false},
18+
{name: "bar", regex: ".*", excluded: []string{"bar"}, expect: false},
19+
{name: "foo", regex: ".*", excluded: []string{"bar"}, expect: true},
20+
{name: "foo", regex: ".*", excluded: []string{"bar", "foo"}, expect: false},
2121
}
2222

2323
for _, tc := range tests {
2424
t.Run(tc.name, func(t *testing.T) {
2525
g := NewWithT(t)
2626

2727
// Test
28-
SetNamespaces(tc.regex, tc.excludeNamespaces...)
29-
isIncluded := IsNamespaceIncluded(tc.name)
28+
SetNamespaces(tc.regex, tc.excluded...)
29+
got := IsManagedNamespace(tc.name)
3030

3131
// Report
32-
g.Expect(isIncluded).Should(Equal(tc.expect))
32+
g.Expect(got).Should(Equal(tc.expect))
3333
})
3434
}
3535

internal/hierarchyconfig/reconciler.go

+21-42
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
9999
log := loggerWithRID(r.Log).WithValues("ns", ns)
100100

101101
// Early exit if it's an excluded namespace
102-
if !config.IsNamespaceIncluded(ns) {
103-
return ctrl.Result{}, r.handleExcludedNamespace(ctx, log, ns)
102+
if !config.IsManagedNamespace(ns) {
103+
return ctrl.Result{}, r.handleUnmanaged(ctx, log, ns)
104104
}
105105

106106
stats.StartHierConfigReconcile()
@@ -109,7 +109,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
109109
return ctrl.Result{}, r.reconcile(ctx, log, ns)
110110
}
111111

112-
func (r *Reconciler) handleExcludedNamespace(ctx context.Context, log logr.Logger, nm string) error {
112+
func (r *Reconciler) handleUnmanaged(ctx context.Context, log logr.Logger, nm string) error {
113113
// Get the namespace. Early exist if the namespace doesn't exist or is purged.
114114
// If so, there must be no namespace label or HC instance to delete.
115115
nsInst, err := r.getNamespace(ctx, nm)
@@ -127,13 +127,23 @@ func (r *Reconciler) handleExcludedNamespace(ctx context.Context, log logr.Logge
127127
return err
128128
}
129129

130-
// Always delete hierarchyconfiguration (and any other HNC CRs) in the
131-
// excluded namespaces. Note: since singletons in the excluded namespaces are
132-
// never synced by HNC, there are no finalizers on the singletons that we can
133-
// delete them without removing the finalizers first.
134-
if err := r.deleteSingletonIfExists(ctx, log, nm); err != nil {
130+
// Don't delete the hierarchy config, since the admin might be enabling and disabling the regex and
131+
// we don't want this to be destructive. Instead, just remove the finalizers if there are any so that
132+
// users can delete it if they like.
133+
inst, _, err := r.getSingleton(ctx, nm)
134+
if err != nil {
135135
return err
136136
}
137+
if len(inst.ObjectMeta.Finalizers) > 0 || len(inst.Status.Children) > 0 {
138+
log.Info("Removing finalizers and children on unmanaged singleton")
139+
inst.ObjectMeta.Finalizers = nil
140+
inst.Status.Children = nil
141+
stats.WriteHierConfig()
142+
if err := r.Update(ctx, inst); err != nil {
143+
log.Error(err, "while removing finalizers on unmanaged namespace")
144+
return err
145+
}
146+
}
137147

138148
return nil
139149
}
@@ -428,9 +438,9 @@ func (r *Reconciler) syncParent(log logr.Logger, inst *api.HierarchyConfiguratio
428438

429439
// Sync this namespace with its current parent.
430440
curParent := r.Forest.Get(inst.Spec.Parent)
431-
if !config.IsNamespaceIncluded(inst.Spec.Parent) {
432-
log.Info("Setting ConditionActivitiesHalted: excluded namespace set as parent", "parent", inst.Spec.Parent)
433-
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonIllegalParent, fmt.Sprintf("Parent %q is an excluded namespace", inst.Spec.Parent))
441+
if !config.IsManagedNamespace(inst.Spec.Parent) {
442+
log.Info("Setting ConditionActivitiesHalted: unmanaged namespace set as parent", "parent", inst.Spec.Parent)
443+
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonIllegalParent, fmt.Sprintf("Parent %q is an unmanaged namespace", inst.Spec.Parent))
434444
} else if curParent != nil && !curParent.Exists() {
435445
log.Info("Setting ConditionActivitiesHalted: parent doesn't exist (or hasn't been synced yet)", "parent", inst.Spec.Parent)
436446
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonParentMissing, fmt.Sprintf("Parent %q does not exist", inst.Spec.Parent))
@@ -644,37 +654,6 @@ func (r *Reconciler) writeHierarchy(ctx context.Context, log logr.Logger, orig,
644654
return true, nil
645655
}
646656

647-
// deleteSingletonIfExists deletes the singleton in the namespace if it exists.
648-
// Note: Make sure there's no finalizers on the singleton before calling this
649-
// function.
650-
func (r *Reconciler) deleteSingletonIfExists(ctx context.Context, log logr.Logger, nm string) error {
651-
inst, deletingCRD, err := r.getSingleton(ctx, nm)
652-
if err != nil {
653-
return err
654-
}
655-
656-
// Early exit if the singleton doesn't exist.
657-
if inst.CreationTimestamp.IsZero() {
658-
return nil
659-
}
660-
661-
// If the CRD is being deleted, we don't need to delete it separately. It will
662-
// be deleted with the CRD.
663-
if deletingCRD {
664-
log.Info("HC in excluded namespace is already being deleted")
665-
return nil
666-
}
667-
log.Info("Deleting illegal HC in excluded namespace")
668-
669-
stats.WriteHierConfig()
670-
if err := r.Delete(ctx, inst); err != nil {
671-
log.Error(err, "while deleting on apiserver")
672-
return err
673-
}
674-
675-
return nil
676-
}
677-
678657
func (r *Reconciler) writeNamespace(ctx context.Context, log logr.Logger, orig, inst *corev1.Namespace) (bool, error) {
679658
if reflect.DeepEqual(orig, inst) {
680659
return false, nil

internal/hierarchyconfig/reconciler_test.go

-11
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ var _ = Describe("Hierarchy", func() {
4141
Eventually(HasChild(ctx, barName, fooName)).Should(Equal(true))
4242
})
4343

44-
It("should remove the hierarchyconfiguration singleton in an excluded namespacee", func() {
45-
// Set the excluded-namespace "kube-system"'s parent to "bar".
46-
config.SetNamespaces("", "kube-system")
47-
exHier := NewHierarchy("kube-system")
48-
exHier.Spec.Parent = barName
49-
UpdateHierarchy(ctx, exHier)
50-
51-
// Verify the hierarchyconfiguration singleton is deleted.
52-
Eventually(CanGetHierarchy(ctx, "kube-system")).Should(Equal(false))
53-
})
54-
5544
It("should set IllegalParent condition if the parent is an excluded namespace", func() {
5645
// Set bar's parent to the excluded-namespace "kube-system".
5746
config.SetNamespaces("", "kube-system")

internal/hierarchyconfig/validator.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ func (v *Validator) handle(ctx context.Context, log logr.Logger, req *request) a
120120
return allow("HNC SA")
121121
}
122122

123-
if !config.IsNamespaceIncluded(req.hc.Namespace) {
124-
reason := fmt.Sprintf("Cannot set the excluded namespace %q as a child of another namespace", req.hc.Namespace)
123+
if why := config.WhyUnmanaged(req.hc.Namespace); why != "" {
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)
125125
return deny(metav1.StatusReasonForbidden, reason)
126126
}
127-
if !config.IsNamespaceIncluded(req.hc.Spec.Parent) {
128-
reason := fmt.Sprintf("Cannot set the parent to the excluded namespace %q", req.hc.Spec.Parent)
127+
if why := config.WhyUnmanaged(req.hc.Spec.Parent); why != "" {
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)
129129
return deny(metav1.StatusReasonForbidden, reason)
130130
}
131131

@@ -333,8 +333,14 @@ func (v *Validator) getServerChecks(log logr.Logger, ns, curParent, newParent *f
333333
}
334334

335335
// If this is currently a root and we're moving into a new tree, just check the parent.
336-
if curParent == nil {
337-
return []serverCheck{{nnm: newParent.Name(), reason: "proposed parent", checkType: checkAuthz}}
336+
//
337+
// Exception: if the current parent is unmanaged (e.g. it used to be managed before, but isn't anymore),
338+
// treat it as though it's currently a root and allow the change.
339+
if curParent == nil || !config.IsManagedNamespace(curParent.Name()) {
340+
if newParent != nil { // could be nil if old parane is unmanaged
341+
return []serverCheck{{nnm: newParent.Name(), reason: "proposed parent", checkType: checkAuthz}}
342+
}
343+
return nil
338344
}
339345

340346
// If the current parent is missing, ask for a missing check. Note that only the *direct* parent

internal/hierarchyconfig/validator_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ func TestStructure(t *testing.T) {
3939
// should see denial message of excluded namespace for `kube-system`. As for
4040
// `kube-public`, we will see missing parent/child instead of excluded
4141
// namespaces denial message for it.
42-
{name: "exclude parent kube-system", nnm: "a", pnm: "kube-system", fail: true, msgContains: "Cannot set the parent to the excluded namespace"},
42+
{name: "exclude parent kube-system", nnm: "a", pnm: "kube-system", fail: true, msgContains: "excluded"},
4343
{name: "missing parent kube-public", nnm: "a", pnm: "kube-public", fail: true, msgContains: "does not exist"},
44-
{name: "exclude child kube-system", nnm: "kube-system", pnm: "a", fail: true, msgContains: "Cannot set the excluded namespace"},
44+
{name: "exclude child kube-system", nnm: "kube-system", pnm: "a", fail: true, msgContains: "excluded"},
4545
{name: "missing child kube-public", nnm: "kube-public", pnm: "a", fail: true, msgContains: "HNC has not reconciled namespace"},
4646
}
4747
for _, tc := range tests {

internal/namespace/mutator.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (m *Mutator) Handle(ctx context.Context, req admission.Request) admission.R
5252
// Currently, we only add `included-namespace` label to non-excluded namespaces
5353
// if the label is missing.
5454
func (m *Mutator) handle(log logr.Logger, ns *corev1.Namespace) {
55-
if !config.IsNamespaceIncluded(ns.Name) {
55+
if !config.IsManagedNamespace(ns.Name) {
5656
return
5757
}
5858

@@ -61,7 +61,7 @@ func (m *Mutator) handle(log logr.Logger, ns *corev1.Namespace) {
6161
if ns.Labels == nil {
6262
ns.Labels = map[string]string{}
6363
}
64-
log.Info("Not an excluded namespace; set included-namespace label.")
64+
log.Info("Managed namespace is missing included-namespace label; adding")
6565
ns.Labels[api.LabelIncludedNamespace] = "true"
6666
}
6767
}

internal/namespace/validator.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,12 @@ func (v *Validator) illegalIncludedNamespaceLabel(req *nsRequest) admission.Resp
120120
return webhooks.Allow("")
121121
}
122122
}
123-
isIncluded := config.IsNamespaceIncluded(req.ns.Name)
123+
124+
isIncluded := config.IsManagedNamespace(req.ns.Name)
124125

125126
// An excluded namespaces should not have included-namespace label.
126127
if !isIncluded && hasLabel {
127-
msg := fmt.Sprintf("You cannot enforce webhook rules on this excluded namespace using the %q label. "+
128+
msg := fmt.Sprintf("You cannot enforce webhook rules on this unmanaged namespace using the %q label. "+
128129
"See https://github.com/kubernetes-sigs/hierarchical-namespaces/blob/master/docs/user-guide/concepts.md#included-namespace-label "+
129130
"for detail.", api.LabelIncludedNamespace)
130131
return webhooks.Deny(metav1.StatusReasonForbidden, msg)
@@ -135,7 +136,7 @@ func (v *Validator) illegalIncludedNamespaceLabel(req *nsRequest) admission.Resp
135136
// Note: since we have a mutating webhook to set the correct label if it's
136137
// missing before this, we only need to check if the label value is correct.
137138
if isIncluded && labelValue != "true" {
138-
msg := fmt.Sprintf("You cannot change the value of the %q label. It has to be set as true on a non-excluded namespace. "+
139+
msg := fmt.Sprintf("You cannot change the value of the %q label. It has to be set as true on all managed namespaces. "+
139140
"See https://github.com/kubernetes-sigs/hierarchical-namespaces/blob/master/docs/user-guide/concepts.md#included-namespace-label "+
140141
"for detail.", api.LabelIncludedNamespace)
141142
return webhooks.Deny(metav1.StatusReasonForbidden, msg)

0 commit comments

Comments
 (0)