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

Commit e1cbb6c

Browse files
authored
Merge pull request #1488 from yiqigao217/condition
Sync object propagation on newly labeled namespaces
2 parents ae0b4fe + 8ace87b commit e1cbb6c

File tree

4 files changed

+138
-11
lines changed

4 files changed

+138
-11
lines changed

incubator/hnc/internal/forest/namespace.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package forest
22

33
import (
4+
"reflect"
5+
46
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
57
"k8s.io/apimachinery/pkg/labels"
68
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -92,12 +94,15 @@ func (ns *Namespace) GetLabels() labels.Set {
9294
return labels.Set(ns.labels)
9395
}
9496

95-
// Deep copy the input labels so that it'll not be changed after
96-
func (ns *Namespace) SetLabels(labels map[string]string) {
97+
// Deep copy the input labels so that it'll not be changed after. It returns
98+
// true if the labels are updated; returns false if there's no change.
99+
func (ns *Namespace) SetLabels(labels map[string]string) bool {
100+
updated := !reflect.DeepEqual(ns.labels, labels)
97101
ns.labels = make(map[string]string)
98102
for key, val := range labels {
99103
ns.labels[key] = val
100104
}
105+
return updated
101106
}
102107

103108
// clean garbage collects this namespace if it has a zero value.

incubator/hnc/internal/reconcilers/hierarchy_config.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,13 @@ func (r *HierarchyConfigReconciler) reconcile(ctx context.Context, log logr.Logg
144144
r.updateFinalizers(ctx, log, inst, nsInst, anms)
145145

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

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

@@ -223,7 +223,8 @@ func (r *HierarchyConfigReconciler) updateFinalizers(ctx context.Context, log lo
223223
// guarded by the forest mutex, which means that none of the other namespaces being reconciled will
224224
// be able to proceed until this one is finished. While the results of the reconiliation may not be
225225
// fully written back to the apiserver yet, each namespace is reconciled in isolation (apart from
226-
// the in-memory forest) so this is fine.
226+
// the in-memory forest) so this is fine. Return true, if the namespace is just synced or the
227+
// namespace labels are changed that requires updating all objects in the namespaces.
227228
func (r *HierarchyConfigReconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, inst *api.HierarchyConfiguration, deletingCRD bool, anms []string) bool {
228229
r.Forest.Lock()
229230
defer r.Forest.Unlock()
@@ -257,9 +258,9 @@ func (r *HierarchyConfigReconciler) syncWithForest(log logr.Logger, nsInst *core
257258
r.syncConditions(log, inst, ns, deletingCRD, hadCrit)
258259

259260
// Sync the tree labels. This should go last since it can depend on the conditions.
260-
r.syncLabel(log, nsInst, ns)
261+
nsCustomerLabelUpdated := r.syncLabel(log, nsInst, ns)
261262

262-
return initial
263+
return initial || nsCustomerLabelUpdated
263264
}
264265

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

423-
func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) {
424+
// Sync namespace tree labels and other labels. Return true if the labels are updated.
425+
func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
424426
if ns.IsExternal() {
425427
metadata.SetLabel(nsInst, nsInst.Name+api.LabelTreeDepthSuffix, "0")
426-
return
428+
return false
427429
}
428430

429431
// Remove all existing depth labels.
@@ -461,7 +463,11 @@ func (r *HierarchyConfigReconciler) syncLabel(log logr.Logger, nsInst *corev1.Na
461463
}
462464
// Update the labels in the forest so that we can quickly access the labels and
463465
// compare if they match the given selector
464-
ns.SetLabels(nsInst.Labels)
466+
if ns.SetLabels(nsInst.Labels) {
467+
log.Info("Namespace labels have been updated.")
468+
return true
469+
}
470+
return false
465471
}
466472

467473
func (r *HierarchyConfigReconciler) syncConditions(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace, deletingCRD, hadCrit bool) {

incubator/hnc/internal/reconcilers/object_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,106 @@ var _ = Describe("Exceptions", func() {
231231
})
232232
}
233233
})
234+
235+
Context("Update the descendant namespaces after 'select' exception annotation is set", func() {
236+
const (
237+
label = "propagate-label"
238+
p = "parent"
239+
labeledchild = "labeledchild"
240+
nolabelchild = "nolabelchild"
241+
labeledns = "labeledns"
242+
nolabelns = "nolabelns"
243+
)
244+
tests := []struct {
245+
name string
246+
toLabel string
247+
toUnlabel string
248+
toAddChild string
249+
want []string
250+
notWant []string
251+
}{{
252+
name: "propagate object only to children with the label",
253+
want: []string{labeledchild},
254+
notWant: []string{nolabelchild},
255+
}, {
256+
name: "propagate object to a newly-labeled child - issue #1448",
257+
toLabel: nolabelchild,
258+
want: []string{labeledchild, nolabelchild},
259+
notWant: []string{},
260+
}, {
261+
name: "not propagate object to a newly-unlabeled child - issue #1448",
262+
toUnlabel: labeledchild,
263+
want: []string{},
264+
notWant: []string{labeledchild, nolabelchild},
265+
}, {
266+
name: "propagate object to a new child with the label",
267+
toAddChild: labeledns,
268+
want: []string{labeledchild, labeledns},
269+
notWant: []string{nolabelchild},
270+
}, {
271+
name: "not propagate object to a new child without the label",
272+
toAddChild: nolabelns,
273+
want: []string{labeledchild},
274+
notWant: []string{nolabelchild, nolabelns},
275+
}}
276+
277+
for _, tc := range tests {
278+
// Making a local copy of tc is necessary to ensure the correct value is passed to the closure,
279+
// for more details look at https://onsi.github.io/ginkgo/ and search for 'closure'
280+
tc := tc
281+
It("Should "+tc.name, func() {
282+
// Set up namespaces
283+
names := map[string]string{
284+
p: createNS(ctx, p),
285+
labeledchild: createNSWithLabel(ctx, labeledchild, map[string]string{label: "true"}),
286+
nolabelchild: createNS(ctx, nolabelchild),
287+
labeledns: createNSWithLabel(ctx, labeledns, map[string]string{label: "true"}),
288+
nolabelns: createNS(ctx, nolabelns),
289+
}
290+
setParent(ctx, names[labeledchild], names[p])
291+
setParent(ctx, names[nolabelchild], names[p])
292+
293+
// Create a Role and verify it's propagated to all children.
294+
makeObject(ctx, api.RoleResource, names[p], "testrole")
295+
Eventually(hasObject(ctx, api.RoleResource, names[labeledchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[labeledchild])
296+
Eventually(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[nolabelchild])
297+
// Add `select` exception annotation with propagate label and verify the
298+
// object is only propagated to children with the label.
299+
updateObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{
300+
api.AnnotationSelector: label,
301+
})
302+
Eventually(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeFalse(), "When propagating testrole to %s", names[nolabelchild])
303+
Consistently(hasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeFalse(), "When propagating testrole to %s", names[nolabelchild])
304+
Consistently(hasObject(ctx, api.RoleResource, names[labeledchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[labeledchild])
305+
306+
// Add the label to the namespace if the value is not empty.
307+
if tc.toLabel != "" {
308+
addNamespaceLabel(ctx, names[tc.toLabel], label, "true")
309+
}
310+
311+
// Unlabel the namespace if the value is not empty.
312+
if tc.toUnlabel != "" {
313+
removeNamespaceLabel(ctx, names[tc.toUnlabel], label)
314+
}
315+
316+
// Set a new child if the value is not empty.
317+
if tc.toAddChild != "" {
318+
setParent(ctx, names[tc.toAddChild], names[p])
319+
}
320+
321+
// then check that the objects are kept in these namespaces
322+
for _, ns := range tc.want {
323+
ns = replaceStrings(ns, names)
324+
Eventually(hasObject(ctx, api.RoleResource, ns, "testrole")).Should(BeTrue(), "When propagating testrole to %s", ns)
325+
}
326+
// make sure the changes are propagated
327+
for _, ns := range tc.notWant {
328+
ns = replaceStrings(ns, names)
329+
Eventually(hasObject(ctx, api.RoleResource, ns, "testrole")).Should(BeFalse(), "When propagating testrole to %s", ns)
330+
}
331+
})
332+
}
333+
})
234334
})
235335

236336
var _ = Describe("Basic propagation", func() {

incubator/hnc/internal/reconcilers/test_helpers_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ func createNSes(ctx context.Context, num int) []string {
108108
return nms
109109
}
110110

111+
func addNamespaceLabel(ctx context.Context, nm, k, v string) {
112+
ns := getNamespace(ctx, nm)
113+
l := ns.Labels
114+
l[k] = v
115+
ns.SetLabels(l)
116+
updateNamespace(ctx, ns)
117+
}
118+
119+
func removeNamespaceLabel(ctx context.Context, nm, k string) {
120+
ns := getNamespace(ctx, nm)
121+
l := ns.Labels
122+
delete(l, k)
123+
ns.SetLabels(l)
124+
updateNamespace(ctx, ns)
125+
}
126+
111127
func updateNamespace(ctx context.Context, ns *corev1.Namespace) {
112128
ExpectWithOffset(1, k8sClient.Update(ctx, ns)).Should(Succeed())
113129
}

0 commit comments

Comments
 (0)