Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

Sync object propagation on newly labeled namespaces #1488

Merged
merged 1 commit into from
Apr 29, 2021
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
9 changes: 7 additions & 2 deletions incubator/hnc/internal/forest/namespace.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package forest

import (
"reflect"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -92,12 +94,15 @@ func (ns *Namespace) GetLabels() labels.Set {
return labels.Set(ns.labels)
}

// Deep copy the input labels so that it'll not be changed after
func (ns *Namespace) SetLabels(labels map[string]string) {
// Deep copy the input labels so that it'll not be changed after. It returns
// true if the labels are updated; returns false if there's no change.
func (ns *Namespace) SetLabels(labels map[string]string) bool {
updated := !reflect.DeepEqual(ns.labels, labels)
ns.labels = make(map[string]string)
for key, val := range labels {
ns.labels[key] = val
}
return updated
}

// clean garbage collects this namespace if it has a zero value.
Expand Down
24 changes: 15 additions & 9 deletions incubator/hnc/internal/reconcilers/hierarchy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ func (r *HierarchyConfigReconciler) reconcile(ctx context.Context, log logr.Logg
r.updateFinalizers(ctx, log, inst, nsInst, anms)

// Sync the Hierarchy singleton with the in-memory forest.
initial := r.syncWithForest(log, nsInst, inst, deletingCRD, anms)
needUpdateObjects := r.syncWithForest(log, nsInst, inst, deletingCRD, anms)

// Write back if anything's changed. Early-exit if we just write back exactly what we had and this
// isn't the first time we're syncing.
updated, err := r.writeInstances(ctx, log, origHC, inst, origNS, nsInst)
updated = updated || initial
if !updated || err != nil {
needUpdateObjects = updated || needUpdateObjects
if !needUpdateObjects || err != nil {
return err
}

Expand Down Expand Up @@ -223,7 +223,8 @@ func (r *HierarchyConfigReconciler) updateFinalizers(ctx context.Context, log lo
// guarded by the forest mutex, which means that none of the other namespaces being reconciled will
// be able to proceed until this one is finished. While the results of the reconiliation may not be
// fully written back to the apiserver yet, each namespace is reconciled in isolation (apart from
// the in-memory forest) so this is fine.
// the in-memory forest) so this is fine. Return true, if the namespace is just synced or the
// namespace labels are changed that requires updating all objects in the namespaces.
func (r *HierarchyConfigReconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, inst *api.HierarchyConfiguration, deletingCRD bool, anms []string) bool {
r.Forest.Lock()
defer r.Forest.Unlock()
Expand Down Expand Up @@ -257,9 +258,9 @@ 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.
r.syncLabel(log, nsInst, ns)
nsCustomerLabelUpdated := r.syncLabel(log, nsInst, ns)

return initial
return initial || nsCustomerLabelUpdated
}

// syncExternalNamespace sets external tree labels to the namespace in the forest
Expand Down Expand Up @@ -420,10 +421,11 @@ func (r *HierarchyConfigReconciler) syncAnchors(log logr.Logger, ns *forest.Name
}
}

func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) {
// 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 {
if ns.IsExternal() {
metadata.SetLabel(nsInst, nsInst.Name+api.LabelTreeDepthSuffix, "0")
return
return false
}

// Remove all existing depth labels.
Expand Down Expand Up @@ -461,7 +463,11 @@ 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
ns.SetLabels(nsInst.Labels)
if ns.SetLabels(nsInst.Labels) {
log.Info("Namespace labels have been updated.")
return true
}
return false
}

func (r *HierarchyConfigReconciler) syncConditions(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace, deletingCRD, hadCrit bool) {
Expand Down
100 changes: 100 additions & 0 deletions incubator/hnc/internal/reconcilers/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,106 @@ var _ = Describe("Exceptions", func() {
})
}
})

Context("Update the descendant namespaces after 'select' exception annotation is set", func() {
adrianludwin marked this conversation as resolved.
Show resolved Hide resolved
const (
label = "propagate-label"
p = "parent"
labeledchild = "labeledchild"
nolabelchild = "nolabelchild"
labeledns = "labeledns"
nolabelns = "nolabelns"
)
tests := []struct {
name string
toLabel string
toUnlabel string
toAddChild string
want []string
notWant []string
}{{
name: "propagate object only to children with the label",
want: []string{labeledchild},
notWant: []string{nolabelchild},
}, {
name: "propagate object to a newly-labeled child - issue #1448",
toLabel: nolabelchild,
want: []string{labeledchild, nolabelchild},
notWant: []string{},
}, {
name: "not propagate object to a newly-unlabeled child - issue #1448",
toUnlabel: labeledchild,
want: []string{},
notWant: []string{labeledchild, nolabelchild},
}, {
name: "propagate object to a new child with the label",
toAddChild: labeledns,
want: []string{labeledchild, labeledns},
notWant: []string{nolabelchild},
}, {
name: "not propagate object to a new child without the label",
toAddChild: nolabelns,
want: []string{labeledchild},
notWant: []string{nolabelchild, nolabelns},
}}

for _, tc := range tests {
// Making a local copy of tc is necessary to ensure the correct value is passed to the closure,
// for more details look at https://onsi.github.io/ginkgo/ and search for 'closure'
tc := tc
It("Should "+tc.name, func() {
// Set up namespaces
names := map[string]string{
p: createNS(ctx, p),
labeledchild: createNSWithLabel(ctx, labeledchild, map[string]string{label: "true"}),
nolabelchild: createNS(ctx, nolabelchild),
labeledns: createNSWithLabel(ctx, labeledns, map[string]string{label: "true"}),
nolabelns: createNS(ctx, nolabelns),
}
setParent(ctx, names[labeledchild], names[p])
setParent(ctx, names[nolabelchild], names[p])

// Create a Role and verify it's propagated to all children.
makeObject(ctx, api.RoleResource, names[p], "testrole")
Eventually(hasObject(ctx, api.RoleResource, names[labeledchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[labeledchild])
Eventually(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[nolabelchild])
// Add `select` exception annotation with propagate label and verify the
// object is only propagated to children with the label.
updateObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{
api.AnnotationSelector: label,
})
Eventually(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeFalse(), "When propagating testrole to %s", names[nolabelchild])
Consistently(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeFalse(), "When propagating testrole to %s", names[nolabelchild])
Consistently(hasObject(ctx, api.RoleResource, names[labeledchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[labeledchild])

// Add the label to the namespace if the value is not empty.
if tc.toLabel != "" {
addNamespaceLabel(ctx, names[tc.toLabel], label, "true")
}

// Unlabel the namespace if the value is not empty.
if tc.toUnlabel != "" {
removeNamespaceLabel(ctx, names[tc.toUnlabel], label)
}

// Set a new child if the value is not empty.
if tc.toAddChild != "" {
setParent(ctx, names[tc.toAddChild], names[p])
}

// then check that the objects are kept in these namespaces
for _, ns := range tc.want {
ns = replaceStrings(ns, names)
Eventually(hasObject(ctx, api.RoleResource, ns, "testrole")).Should(BeTrue(), "When propagating testrole to %s", ns)
}
// make sure the changes are propagated
for _, ns := range tc.notWant {
ns = replaceStrings(ns, names)
Eventually(hasObject(ctx, api.RoleResource, ns, "testrole")).Should(BeFalse(), "When propagating testrole to %s", ns)
}
})
}
})
})

var _ = Describe("Basic propagation", func() {
Expand Down
16 changes: 16 additions & 0 deletions incubator/hnc/internal/reconcilers/test_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ func createNSes(ctx context.Context, num int) []string {
return nms
}

func addNamespaceLabel(ctx context.Context, nm, k, v string) {
ns := getNamespace(ctx, nm)
l := ns.Labels
l[k] = v
ns.SetLabels(l)
updateNamespace(ctx, ns)
}

func removeNamespaceLabel(ctx context.Context, nm, k string) {
ns := getNamespace(ctx, nm)
l := ns.Labels
delete(l, k)
ns.SetLabels(l)
updateNamespace(ctx, ns)
}

func updateNamespace(ctx context.Context, ns *corev1.Namespace) {
ExpectWithOffset(1, k8sClient.Update(ctx, ns)).Should(Succeed())
}
Expand Down