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

Commit 082779a

Browse files
authored
Merge pull request #125 from nobbs/dev/per-object-filtering
Add per-object-level filtering based on labels (excluding Rancher-generated resources)
2 parents 63fde24 + 3e7ec48 commit 082779a

File tree

4 files changed

+83
-14
lines changed

4 files changed

+83
-14
lines changed

docs/user-guide/concepts.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,14 @@ adding annotations is not possible and HNC will by default exclude them.
361361
Similarly, Kubernetes ServiceAccount Secrets will also by default be excluded
362362
from propagation.
363363

364+
In addition, propagation exclusions are also used for Rancher-managed Kubernetes
365+
clusters. Rancher uses a "project" concept that bundles namespaces and thus sets
366+
roles, rolebindings, etc. for all namespaces of a project. This leads to
367+
conflicts with HNC, so all resources created by Rancher (which are automatically
368+
labeled with `"cattle.io/creator": "norman"` by Rancher, cf. [their
369+
docs](https://rancher.com/docs/rancher/v2.6/en/system-tools/#remove)) are
370+
excluded from propagation.
371+
364372
<a name="admin"/>
365373

366374
## Administration

internal/integtest/helpers.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,9 @@ func MakeObject(ctx context.Context, resource string, nsName, name string) {
300300
createdObjects = append(createdObjects, inst)
301301
}
302302

303-
// MakeObjectWithAnnotation creates an empty object with annotation given kind in a specific
303+
// MakeObjectWithAnnotations creates an empty object with annotation given kind in a specific
304304
// namespace. The kind and its corresponding GVK should be included in the GVKs map.
305-
func MakeObjectWithAnnotation(ctx context.Context, resource string, nsName,
305+
func MakeObjectWithAnnotations(ctx context.Context, resource string, nsName,
306306
name string, a map[string]string) {
307307
inst := &unstructured.Unstructured{}
308308
inst.SetGroupVersionKind(GVKs[resource])
@@ -313,9 +313,22 @@ func MakeObjectWithAnnotation(ctx context.Context, resource string, nsName,
313313
createdObjects = append(createdObjects, inst)
314314
}
315315

316-
// UpdateObjectWithAnnotation gets an object given it's kind, nsName and name, adds the annotation
316+
// MakeObjectWithLabels creates an empty object with label given kind in a specific
317+
// namespace. The kind and its corresponding GVK should be included in the GVKs map.
318+
func MakeObjectWithLabels(ctx context.Context, resource string, nsName,
319+
name string, l map[string]string) {
320+
inst := &unstructured.Unstructured{}
321+
inst.SetGroupVersionKind(GVKs[resource])
322+
inst.SetNamespace(nsName)
323+
inst.SetName(name)
324+
inst.SetLabels(l)
325+
ExpectWithOffset(1, K8sClient.Create(ctx, inst)).Should(Succeed())
326+
createdObjects = append(createdObjects, inst)
327+
}
328+
329+
// UpdateObjectWithAnnotations gets an object given it's kind, nsName and name, adds the annotation
317330
// and updates this object
318-
func UpdateObjectWithAnnotation(ctx context.Context, resource string, nsName,
331+
func UpdateObjectWithAnnotations(ctx context.Context, resource string, nsName,
319332
name string, a map[string]string) error {
320333
nnm := types.NamespacedName{Namespace: nsName, Name: name}
321334
inst := &unstructured.Unstructured{}

internal/objects/reconciler_test.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ var _ = Describe("Exceptions", func() {
142142
tc.treeSelector = ReplaceStrings(tc.treeSelector, names)
143143

144144
// Create a Role with the selector and treeSelector annotation
145-
MakeObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{
145+
MakeObjectWithAnnotations(ctx, api.RoleResource, names[p], "testrole", map[string]string{
146146
api.AnnotationSelector: tc.selector,
147147
api.AnnotationTreeSelector: tc.treeSelector,
148148
api.AnnotationNoneSelector: tc.noneSelector,
@@ -215,7 +215,7 @@ var _ = Describe("Exceptions", func() {
215215
}
216216

217217
// update the role with the selector and treeSelector annotation
218-
UpdateObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{
218+
UpdateObjectWithAnnotations(ctx, api.RoleResource, names[p], "testrole", map[string]string{
219219
api.AnnotationSelector: tc.selector,
220220
api.AnnotationTreeSelector: tc.treeSelector,
221221
api.AnnotationNoneSelector: tc.noneSelector,
@@ -232,7 +232,7 @@ var _ = Describe("Exceptions", func() {
232232
}
233233

234234
// remove the annotation and verify that the object is back for every namespace
235-
UpdateObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{})
235+
UpdateObjectWithAnnotations(ctx, api.RoleResource, names[p], "testrole", map[string]string{})
236236
for _, ns := range names {
237237
Eventually(HasObject(ctx, api.RoleResource, ns, "testrole")).Should(BeTrue(), "When propagating testrole to %s", ns)
238238
}
@@ -304,7 +304,7 @@ var _ = Describe("Exceptions", func() {
304304
Eventually(HasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeTrue(), "When propagating testrole to %s", names[nolabelchild])
305305
// Add `select` exception annotation with propagate label and verify the
306306
// object is only propagated to children with the label.
307-
UpdateObjectWithAnnotation(ctx, api.RoleResource, names[p], "testrole", map[string]string{
307+
UpdateObjectWithAnnotations(ctx, api.RoleResource, names[p], "testrole", map[string]string{
308308
api.AnnotationSelector: label,
309309
})
310310
Eventually(HasObject(ctx, api.RoleResource, names[nolabelchild], "testrole")).Should(BeFalse(), "When propagating testrole to %s", names[nolabelchild])
@@ -398,7 +398,7 @@ var _ = Describe("Basic propagation", func() {
398398
Expect(ObjectInheritedFrom(ctx, "configmaps", barName, "foo-config")).Should(Equal(fooName))
399399
})
400400

401-
It("should not propagate builtin exclusions", func() {
401+
It("should not propagate builtin exclusions by name", func() {
402402
SetParent(ctx, barName, fooName)
403403
MakeObject(ctx, "configmaps", fooName, "istio-ca-root-cert")
404404
MakeObject(ctx, "configmaps", fooName, "kube-root-ca.crt")
@@ -411,6 +411,27 @@ var _ = Describe("Basic propagation", func() {
411411
Eventually(HasObject(ctx, "configmaps", barName, "kube-root-ca.crt")).Should(BeFalse())
412412
})
413413

414+
It("should not propagate builtin exclusions by labels", func() {
415+
SetParent(ctx, barName, fooName)
416+
MakeObjectWithLabels(ctx, "roles", fooName, "role-with-labels-blocked", map[string]string{"cattle.io/creator": "norman"})
417+
MakeObjectWithLabels(ctx, "roles", fooName, "role-with-labels-wrong-value", map[string]string{"cattle.io/creator": "testme"})
418+
MakeObjectWithLabels(ctx, "roles", fooName, "role-with-labels-something", map[string]string{"app": "hello-world"})
419+
AddToHNCConfig(ctx, api.RBACGroup, api.RoleKind, api.Propagate)
420+
421+
// the first one should not propagate, everything else should
422+
Consistently(HasObject(ctx, "roles", barName, "role-with-labels-blocked")).Should(BeFalse())
423+
Eventually(HasObject(ctx, "roles", barName, "role-with-labels-wrong-value")).Should(BeTrue())
424+
Eventually(HasObject(ctx, "roles", barName, "role-with-labels-something")).Should(BeTrue())
425+
426+
// lets try the same with config maps, they are also filtered and should not propagate
427+
MakeObjectWithLabels(ctx, "configmaps", fooName, "cm-with-label-1", map[string]string{"cattle.io/creator": "norman"})
428+
MakeObjectWithLabels(ctx, "configmaps", fooName, "cm-with-label-2", map[string]string{"app": "hello-world"})
429+
AddToHNCConfig(ctx, "", "configmaps", api.Propagate)
430+
431+
Consistently(HasObject(ctx, "configmaps", barName, "cm-with-label-1")).Should(BeFalse())
432+
Eventually(HasObject(ctx, "configmaps", barName, "cm-with-label-2")).Should(BeTrue())
433+
})
434+
414435
It("should be removed if the hierarchy changes", func() {
415436
SetParent(ctx, barName, fooName)
416437
SetParent(ctx, bazName, barName)
@@ -658,7 +679,7 @@ var _ = Describe("Basic propagation", func() {
658679

659680
It("should avoid propagating banned annotations", func() {
660681
SetParent(ctx, barName, fooName)
661-
MakeObjectWithAnnotation(ctx, "roles", fooName, "foo-annot-role", map[string]string{
682+
MakeObjectWithAnnotations(ctx, "roles", fooName, "foo-annot-role", map[string]string{
662683
"annot-a": "value-a",
663684
"annot-b": "value-b",
664685
})
@@ -682,7 +703,7 @@ var _ = Describe("Basic propagation", func() {
682703

683704
// Tell the HNC config not to propagate annot-a and verify that this time, it's not annotated
684705
config.UnpropagatedAnnotations = []string{"annot-a"}
685-
MakeObjectWithAnnotation(ctx, "roles", fooName, "foo-annot-role", map[string]string{
706+
MakeObjectWithAnnotations(ctx, "roles", fooName, "foo-annot-role", map[string]string{
686707
"annot-a": "value-a",
687708
"annot-b": "value-b",
688709
})

internal/selectors/selectors.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,46 @@ func GetNoneSelector(inst *unstructured.Unstructured) (bool, error) {
158158
return noneSelector, nil
159159
}
160160

161-
// cmExclusions are known (istio and kube-root) CA configmap which are excluded from propagation
162-
var cmExclusions = []string{"istio-ca-root-cert", "kube-root-ca.crt"}
161+
// cmExclusionsByName are known (istio and kube-root) CA configmap which are excluded from propagation
162+
var cmExclusionsByName = []string{"istio-ca-root-cert", "kube-root-ca.crt"}
163+
164+
// A label as a key, value pair, used to exclude resources matching this label (both key and value).
165+
type ExclusionByLabelsSpec struct {
166+
Key string
167+
Value string
168+
}
169+
170+
// ExclusionByLabelsSpec are known label key-value pairs which are excluded from propagation. Right
171+
// now only used to exclude resources created by Rancher, see "System Tools > Remove"
172+
// (https://rancher.com/docs/rancher/v2.6/en/system-tools/#remove)
173+
var exclusionByLabels = []ExclusionByLabelsSpec{
174+
{Key: "cattle.io/creator", Value: "norman"},
175+
}
163176

164177
// isExcluded returns true to indicate that this object is excluded from being propagated
165178
func isExcluded(inst *unstructured.Unstructured) (bool, error) {
166179
name := inst.GetName()
167180
kind := inst.GetKind()
168181
group := inst.GroupVersionKind().Group
182+
labels := inst.GetLabels()
169183

170-
for _, excludedResourceName := range cmExclusions {
184+
// exclusion by name
185+
for _, excludedResourceName := range cmExclusionsByName {
171186
if group == "" && kind == "ConfigMap" && name == excludedResourceName {
172187
return true, nil
173188
}
174189
}
190+
191+
// exclusion by labels
192+
for _, res := range exclusionByLabels {
193+
gotLabelValue, ok := labels[res.Key]
194+
// check for presence has to be explicit, as empty label values are allowed and a
195+
// nonexisting key in the `labels` map will also return an empty string ("") - potentially
196+
// causing false matches if `ok` is not checked
197+
if ok && gotLabelValue == res.Value {
198+
return true, nil
199+
}
200+
}
201+
175202
return false, nil
176203
}

0 commit comments

Comments
 (0)