Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions internal/anchor/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,29 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
return ctrl.Result{}, err
}

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

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

// Get the subnamespace. If it doesn't exist, initialize one.
Expand Down
12 changes: 6 additions & 6 deletions internal/anchor/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ func (v *Validator) handle(req *anchorRequest) admission.Response {

switch req.op {
case k8sadm.Create:
// Can't create subnamespaces in excluded namespaces
if !config.IsNamespaceIncluded(pnm) {
msg := fmt.Sprintf("Cannot create a subnamespace in the excluded namespace %q", pnm)
// Can't create subnamespaces in unmanaged namespaces
if why := config.WhyUnmanaged(pnm); why != "" {
msg := fmt.Sprintf("Cannot create a subnamespace in the unmanaged namespace %q (%s)", pnm, why)
return webhooks.Deny(metav1.StatusReasonForbidden, msg)
}
// Can't create subnamespaces using excluded namespace names
if !config.IsNamespaceIncluded(cnm) {
msg := fmt.Sprintf("Cannot create a subnamespace using the excluded namespace name %q", cnm)
// Can't create subnamespaces using unmanaged namespace names
if why := config.WhyUnmanaged(cnm); why != "" {
msg := fmt.Sprintf("Cannot create a subnamespace using the unmanaged namespace name %q (%s)", cnm, why)
return webhooks.Deny(metav1.StatusReasonForbidden, msg)
}

Expand Down
10 changes: 0 additions & 10 deletions internal/config/default_config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package config

import "regexp"

// UnpropgatedAnnotations is a list of annotations on objects that should _not_ be propagated by HNC.
// Much like HNC itself, other systems (such as GKE Config Sync) use annotations to "claim" an
// object - such as deleting objects it doesn't recognize. By removing these annotations on
Expand All @@ -10,11 +8,3 @@ import "regexp"
// This value is controlled by the --unpropagated-annotation command line, which may be set multiple
// times.
var UnpropagatedAnnotations []string

// ExcludedNamespaces is a list of namespaces used by reconcilers and validators
// to exclude namespaces that shouldn't be reconciled or validated.
//
// This value is controlled by the --excluded-namespace command line, which may
// be set multiple times.
var excludedNamespaces map[string]bool
var includedNamespacesRegex *regexp.Regexp
47 changes: 41 additions & 6 deletions internal/config/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,62 @@ import (
"regexp"
)

var (
// excludedNamespaces is a list of namespaces used by reconcilers and validators
// to exclude namespaces that shouldn't be reconciled or validated.
//
// This value is controlled by the --excluded-namespace command line, which may
// be set multiple times.
excludedNamespaces map[string]bool

// includedNamespacesRegex is the compiled regex of included namespaces
includedNamespacesRegex *regexp.Regexp

// includedNamespacesStr is the original pattern of the regex. It must
// only be used to generate error messages.
includedNamespacesStr string
)

func SetNamespaces(regex string, excluded ...string) {
if regex == "" {
regex = ".*"
}

includedNamespacesRegex = regexp.MustCompile("^" + regex + "$")
includedNamespacesStr = "^" + regex + "$"
includedNamespacesRegex = regexp.MustCompile(includedNamespacesStr)

excludedNamespaces = make(map[string]bool)
for _, exn := range excluded {
excludedNamespaces[exn] = true
}
}

func IsNamespaceIncluded(name string) bool {
if excludedNamespaces[name] {
return false
// WhyUnamanged returns a human-readable message explaining why the given
// namespace is unmanaged, or an empty string if it *is* managed.
func WhyUnmanaged(nm string) string {
// We occasionally check if the _parent_ of a namespace is managed.
// It's an error for a managed namespace to have an unmanaged parent,
// so we'll treat "no parent" as managed.
if nm == "" {
return ""
}

if excludedNamespaces[nm] {
return "excluded by the HNC administrator"
}

if includedNamespacesRegex == nil { // unit tests
return true
return ""
}

if !includedNamespacesRegex.MatchString(nm) {
return "does not match the regex set by the HNC administrator: `" + includedNamespacesStr + "`"
}

return includedNamespacesRegex.MatchString(name)
return ""
}

// IsManagedNamespace is the same as WhyUnmanaged but converts the response to a bool for convenience.
func IsManagedNamespace(nm string) bool {
return WhyUnmanaged(nm) == ""
}
24 changes: 12 additions & 12 deletions internal/config/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@ import (
func TestIsNamespaceIncluded(t *testing.T) {

tests := []struct {
name string
regex string
excludeNamespaces []string
expect bool
name string
regex string
excluded []string
expect bool
}{
{name: "foobar", regex: "foo.*", excludeNamespaces: []string{"bar"}, expect: true},
{name: "bar", regex: "foo-.*", excludeNamespaces: []string{"bar"}, expect: false},
{name: "bar", regex: ".*", excludeNamespaces: []string{"bar"}, expect: false},
{name: "foo", regex: ".*", excludeNamespaces: []string{"bar"}, expect: true},
{name: "foo", regex: ".*", excludeNamespaces: []string{"bar", "foo"}, expect: false},
{name: "foobar", regex: "foo.*", excluded: []string{"bar"}, expect: true},
{name: "bar", regex: "foo-.*", excluded: []string{"bar"}, expect: false},
{name: "bar", regex: ".*", excluded: []string{"bar"}, expect: false},
{name: "foo", regex: ".*", excluded: []string{"bar"}, expect: true},
{name: "foo", regex: ".*", excluded: []string{"bar", "foo"}, expect: false},
}

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

// Test
SetNamespaces(tc.regex, tc.excludeNamespaces...)
isIncluded := IsNamespaceIncluded(tc.name)
SetNamespaces(tc.regex, tc.excluded...)
got := IsManagedNamespace(tc.name)

// Report
g.Expect(isIncluded).Should(Equal(tc.expect))
g.Expect(got).Should(Equal(tc.expect))
})
}

Expand Down
63 changes: 21 additions & 42 deletions internal/hierarchyconfig/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
log := loggerWithRID(r.Log).WithValues("ns", ns)

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

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

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

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

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

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

// deleteSingletonIfExists deletes the singleton in the namespace if it exists.
// Note: Make sure there's no finalizers on the singleton before calling this
// function.
func (r *Reconciler) deleteSingletonIfExists(ctx context.Context, log logr.Logger, nm string) error {
inst, deletingCRD, err := r.getSingleton(ctx, nm)
if err != nil {
return err
}

// Early exit if the singleton doesn't exist.
if inst.CreationTimestamp.IsZero() {
return nil
}

// If the CRD is being deleted, we don't need to delete it separately. It will
// be deleted with the CRD.
if deletingCRD {
log.Info("HC in excluded namespace is already being deleted")
return nil
}
log.Info("Deleting illegal HC in excluded namespace")

stats.WriteHierConfig()
if err := r.Delete(ctx, inst); err != nil {
log.Error(err, "while deleting on apiserver")
return err
}

return nil
}

func (r *Reconciler) writeNamespace(ctx context.Context, log logr.Logger, orig, inst *corev1.Namespace) (bool, error) {
if reflect.DeepEqual(orig, inst) {
return false, nil
Expand Down
11 changes: 0 additions & 11 deletions internal/hierarchyconfig/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,6 @@ var _ = Describe("Hierarchy", func() {
Eventually(HasChild(ctx, barName, fooName)).Should(Equal(true))
})

It("should remove the hierarchyconfiguration singleton in an excluded namespacee", func() {
// Set the excluded-namespace "kube-system"'s parent to "bar".
config.SetNamespaces("", "kube-system")
exHier := NewHierarchy("kube-system")
exHier.Spec.Parent = barName
UpdateHierarchy(ctx, exHier)

// Verify the hierarchyconfiguration singleton is deleted.
Eventually(CanGetHierarchy(ctx, "kube-system")).Should(Equal(false))
})

It("should set IllegalParent condition if the parent is an excluded namespace", func() {
// Set bar's parent to the excluded-namespace "kube-system".
config.SetNamespaces("", "kube-system")
Expand Down
18 changes: 12 additions & 6 deletions internal/hierarchyconfig/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ func (v *Validator) handle(ctx context.Context, log logr.Logger, req *request) a
return allow("HNC SA")
}

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

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

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

// If the current parent is missing, ask for a missing check. Note that only the *direct* parent
Expand Down
4 changes: 2 additions & 2 deletions internal/hierarchyconfig/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ func TestStructure(t *testing.T) {
// should see denial message of excluded namespace for `kube-system`. As for
// `kube-public`, we will see missing parent/child instead of excluded
// namespaces denial message for it.
{name: "exclude parent kube-system", nnm: "a", pnm: "kube-system", fail: true, msgContains: "Cannot set the parent to the excluded namespace"},
{name: "exclude parent kube-system", nnm: "a", pnm: "kube-system", fail: true, msgContains: "excluded"},
{name: "missing parent kube-public", nnm: "a", pnm: "kube-public", fail: true, msgContains: "does not exist"},
{name: "exclude child kube-system", nnm: "kube-system", pnm: "a", fail: true, msgContains: "Cannot set the excluded namespace"},
{name: "exclude child kube-system", nnm: "kube-system", pnm: "a", fail: true, msgContains: "excluded"},
{name: "missing child kube-public", nnm: "kube-public", pnm: "a", fail: true, msgContains: "HNC has not reconciled namespace"},
}
for _, tc := range tests {
Expand Down
4 changes: 2 additions & 2 deletions internal/namespace/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (m *Mutator) Handle(ctx context.Context, req admission.Request) admission.R
// Currently, we only add `included-namespace` label to non-excluded namespaces
// if the label is missing.
func (m *Mutator) handle(log logr.Logger, ns *corev1.Namespace) {
if !config.IsNamespaceIncluded(ns.Name) {
if !config.IsManagedNamespace(ns.Name) {
return
}

Expand All @@ -61,7 +61,7 @@ func (m *Mutator) handle(log logr.Logger, ns *corev1.Namespace) {
if ns.Labels == nil {
ns.Labels = map[string]string{}
}
log.Info("Not an excluded namespace; set included-namespace label.")
log.Info("Managed namespace is missing included-namespace label; adding")
ns.Labels[api.LabelIncludedNamespace] = "true"
}
}
Expand Down
7 changes: 4 additions & 3 deletions internal/namespace/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,12 @@ func (v *Validator) illegalIncludedNamespaceLabel(req *nsRequest) admission.Resp
return webhooks.Allow("")
}
}
isIncluded := config.IsNamespaceIncluded(req.ns.Name)

isIncluded := config.IsManagedNamespace(req.ns.Name)

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