Skip to content

Commit 5b4f12a

Browse files
committed
Initial impl of managed labels/annotations
See kubernetes-retired#47. This is the initial implementation of managed labels and annotations - that is, the ability to set a label (or annotation) in a HierarchyConfiguration object, and have that label (...) propagated to all descendants, similar to the way objects are propagated. As with objects, only allowlisted labels are propagated, as defined by the command line option '--managed-namespace-[labels|annotations]'. Still to come: validator support, better conditions for conflicts, better testing for external namespaces. Tested: see new integ tests.
1 parent ce8668f commit 5b4f12a

File tree

7 files changed

+246
-32
lines changed

7 files changed

+246
-32
lines changed

api/v1alpha2/hierarchy_types.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,18 @@ type HierarchyConfigurationSpec struct {
124124
// AllowCascadingDeletion indicates if the subnamespaces of this namespace are
125125
// allowed to cascading delete.
126126
AllowCascadingDeletion bool `json:"allowCascadingDeletion,omitempty"`
127+
128+
// Lables is a list of labels and values to apply to the current namespace and all of its
129+
// descendants. All label keys must be specified on the command line by
130+
// --managed-namespace-labels. A namespace cannot have a KVP that conflicts with one of its
131+
// ancestors.
132+
Labels []MetaKVP `json:"labels,omitempty"`
133+
134+
// Annotations is a list of annotations and values to apply to the current namespace and all of
135+
// its descendants. All annotation keys must be specified on the command line by
136+
// --managed-namespace-annotations. A namespace cannot have a KVP that conflicts with one of its
137+
// ancestors.
138+
Annotations []MetaKVP `json:"annotations,omitempty"`
127139
}
128140

129141
// HierarchyStatus defines the observed state of Hierarchy
@@ -147,6 +159,15 @@ type HierarchyConfigurationList struct {
147159
Items []HierarchyConfiguration `json:"items"`
148160
}
149161

162+
// MetaKVP represents a label or annotation
163+
type MetaKVP struct {
164+
// Name is the name of the label or annotation.
165+
Name string `json:"name"`
166+
167+
// Value is the value of the label or annotation.
168+
Value string `json:"value"`
169+
}
170+
150171
// metav1.Condition is introduced in k8s.io/apimachinery v0.20.0-alpha.1 and we
151172
// don't want to take a dependency on it yet, thus we copied the below struct from
152173
// https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/types.go:

api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 26 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/manager/main.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ var (
6565
restartOnSecretRefresh bool
6666
unpropagatedAnnotations arrayArg
6767
excludedNamespaces arrayArg
68+
managedNamespaceLables arrayArg
69+
managedNamespaceAnnots arrayArg
6870
includedNamespacesRegex string
6971
)
7072

@@ -99,11 +101,29 @@ func main() {
99101
flag.Var(&excludedNamespaces, "excluded-namespace", "A namespace that, if present, will be excluded from HNC management. May be specified multiple times, with each instance specifying one namespace. See the user guide for more information.")
100102
flag.StringVar(&includedNamespacesRegex, "included-namespace-regex", ".*", "Namespace regular expression. Namespaces that match this regexp will be included and handle by HNC. As it is a regex, this parameter cannot be specified multiple times. Implicit wrapping of the expression \"^...$\" is done here")
101103
flag.BoolVar(&restartOnSecretRefresh, "cert-restart-on-secret-refresh", false, "Kills the process when secrets are refreshed so that the pod can be restarted (secrets take up to 60s to be updated by running pods)")
104+
flag.Var(&managedNamespaceLables, "managed-namespace-labels", "A list of labels on namespaces that are managed by HNC. These labels may only be set via the HierarchyConfiguration object. See the user guide for more information.")
105+
flag.Var(&managedNamespaceAnnots, "managed-namespace-annotations", "A list of annotations on namespaces that are managed by HNC. These labels may only be set via the HierarchyConfiguration object. See the user guide for more details.")
102106
flag.Parse()
107+
103108
// Assign the array args to the configuration variables after the args are parsed.
104109
config.UnpropagatedAnnotations = unpropagatedAnnotations
105-
106110
config.SetNamespaces(includedNamespacesRegex, excludedNamespaces...)
111+
config.ManagedNamespaceLabels = map[string]bool{}
112+
for _, l := range managedNamespaceLables {
113+
if strings.Contains(l, v1a2.MetaGroup) {
114+
setupLog.Info("Error: cannot use --managed-namespace-labels to control labels in the " + v1a2.MetaGroup + " group")
115+
os.Exit(1)
116+
}
117+
config.ManagedNamespaceLabels[l] = true
118+
}
119+
config.ManagedNamespaceAnnotations = map[string]bool{}
120+
for _, a := range managedNamespaceAnnots {
121+
if strings.Contains(a, v1a2.MetaGroup) {
122+
setupLog.Info("Error: cannot use --managed-namespace-annotations to control annotations in the " + v1a2.MetaGroup + " group")
123+
os.Exit(1)
124+
}
125+
config.ManagedNamespaceAnnotations[a] = true
126+
}
107127

108128
// Enable OpenCensus exporters to export metrics
109129
// to Stackdriver Monitoring.

config/crd/bases/hnc.x-k8s.io_hierarchyconfigurations.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,44 @@ spec:
4646
description: AllowCascadingDeletion indicates if the subnamespaces
4747
of this namespace are allowed to cascading delete.
4848
type: boolean
49+
annotations:
50+
description: Annotations is a list of annotations and values to apply
51+
to the current namespace and all of its descendants. All annotation
52+
keys must be specified on the command line by --managed-namespace-annotations.
53+
A namespace cannot have a KVP that conflicts with one of its ancestors.
54+
items:
55+
description: MetaKVP represents a label or annotation
56+
properties:
57+
name:
58+
description: Name is the name of the label or annotation.
59+
type: string
60+
value:
61+
description: Value is the value of the label or annotation.
62+
type: string
63+
required:
64+
- name
65+
- value
66+
type: object
67+
type: array
68+
labels:
69+
description: Lables is a list of labels and values to apply to the
70+
current namespace and all of its descendants. All label keys must
71+
be specified on the command line by --managed-namespace-labels.
72+
A namespace cannot have a KVP that conflicts with one of its ancestors.
73+
items:
74+
description: MetaKVP represents a label or annotation
75+
properties:
76+
name:
77+
description: Name is the name of the label or annotation.
78+
type: string
79+
value:
80+
description: Value is the value of the label or annotation.
81+
type: string
82+
required:
83+
- name
84+
- value
85+
type: object
86+
type: array
4987
parent:
5088
description: Parent indicates the parent of this namespace, if any.
5189
type: string

internal/config/default_config.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
package config
22

3-
// UnpropgatedAnnotations is a list of annotations on objects that should _not_ be propagated by HNC.
4-
// Much like HNC itself, other systems (such as GKE Config Sync) use annotations to "claim" an
5-
// object - such as deleting objects it doesn't recognize. By removing these annotations on
6-
// propgated objects, HNC ensures that other systems won't attempt to claim the same object.
7-
//
8-
// This value is controlled by the --unpropagated-annotation command line, which may be set multiple
9-
// times.
10-
var UnpropagatedAnnotations []string
3+
var (
4+
// UnpropgatedAnnotations is a list of annotations on objects that should _not_ be propagated by HNC.
5+
// Much like HNC itself, other systems (such as GKE Config Sync) use annotations to "claim" an
6+
// object - such as deleting objects it doesn't recognize. By removing these annotations on
7+
// propgated objects, HNC ensures that other systems won't attempt to claim the same object.
8+
//
9+
// This value is controlled by the --unpropagated-annotation command line, which may be set multiple
10+
// times.
11+
UnpropagatedAnnotations []string
12+
13+
// ManagedNamespaceLabels is a set of labels whose values are controlled by the "labels" field in
14+
// the HierarchyController CR. Any label in this list is removed from all managed namespaces
15+
// unless specifically specified by the HC of the namespace or one of its ancestors.
16+
ManagedNamespaceLabels map[string]bool
17+
18+
// ManagedNamespaceAnnotations is a set of annotations whose values are controlled by the "labels" field in
19+
// the HierarchyController CR. Any label in this list is removed from all managed namespaces
20+
// unless specifically specified by the HC of the namespace or one of its ancestors.
21+
ManagedNamespaceAnnotations map[string]bool
22+
)

internal/forest/namespace.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ type Namespace struct {
3232
// and to store the tree labels of external namespaces.
3333
labels map[string]string
3434

35+
// ManagedLabels are all managed labels explicitly set on this namespace (i.e., excluding anything
36+
// set by ancestors).
37+
ManagedLabels map[string]string
38+
39+
// ManagedAnnotations are all managed annotations explicitly set on this namespace (i.e.,
40+
// excluding anything set by ancestors).
41+
ManagedAnnotations map[string]string
42+
3543
// sourceObjects store the objects created by users, identified by GVK and name.
3644
// It serves as the source of truth for object controllers to propagate objects.
3745
sourceObjects objects

internal/hierarchyconfig/reconciler.go

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ func (r *Reconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, i
311311
initial := r.markExisting(log, ns)
312312

313313
// Sync labels and annotations, now that the structure's been updated.
314-
nsCustomerLabelUpdated := r.syncTreeLabels(log, nsInst, ns)
314+
updatedLabels := r.syncLabels(log, inst, nsInst, ns)
315+
r.syncAnnotations(log, inst, nsInst, ns)
315316

316317
// Sync other spec and spec-like info
317318
r.syncAnchors(log, ns, anms)
@@ -325,7 +326,7 @@ func (r *Reconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, i
325326
r.syncConditions(log, inst, ns, wasHalted)
326327

327328
r.HNCConfigReconciler.Enqueue("namespace reconciled")
328-
return initial || nsCustomerLabelUpdated
329+
return initial || updatedLabels
329330
}
330331

331332
// syncExternalNamespace sets external tree labels to the namespace in the forest
@@ -488,33 +489,60 @@ func (r *Reconciler) syncAnchors(log logr.Logger, ns *forest.Namespace, anms []s
488489
}
489490
}
490491

491-
// Sync namespace tree labels. Return true if the labels are updated.
492-
func (r *Reconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
492+
// Sync namespace managed and tree labels. Return true if the labels are updated, so we know to
493+
// re-sync all objects that could be affected by exclusions.
494+
func (r *Reconciler) syncLabels(log logr.Logger, inst *api.HierarchyConfiguration, nsInst *corev1.Namespace, ns *forest.Namespace) bool {
495+
// Get a list of all managed labels from the config object.
496+
managed := map[string]string{}
497+
for _, kvp := range inst.Spec.Labels {
498+
managed[kvp.Name] = kvp.Value
499+
}
500+
501+
// External namespaces can also have both managed and tree labels set directly. All other
502+
// namespaces must have these removed now.
503+
for k, v := range nsInst.Labels {
504+
if config.ManagedNamespaceLabels[k] {
505+
if ns.IsExternal() {
506+
managed[k] = v
507+
} else {
508+
delete(nsInst.Labels, k)
509+
}
510+
}
511+
if !ns.IsExternal() && strings.HasSuffix(k, api.LabelTreeDepthSuffix) {
512+
delete(nsInst.Labels, k)
513+
}
514+
}
515+
516+
// Record all managed labels in the forest. If they've changed, we need to enqueue all descendants
517+
// to propagate the changes.
518+
if !reflect.DeepEqual(ns.ManagedLabels, managed) {
519+
ns.ManagedLabels = managed
520+
r.enqueueAffected(log, "managed labels have changed", ns.DescendantNames()...)
521+
}
522+
523+
// For external namespaces, we're pretty much done since HNC mainly doesn't manage the metadata of
524+
// external namespaces. Just make sure the zero-depth tree label is set if it wasn't already, and
525+
// store all its labels so the tree labels can be retrieved later.
493526
if ns.IsExternal() {
494527
metadata.SetLabel(nsInst, nsInst.Name+api.LabelTreeDepthSuffix, "0")
495-
496-
// Set the labels so we can retrieve the external tree labels in the future if needed
497528
ns.SetLabels(nsInst.Labels)
498-
return false
499-
}
500529

501-
// Remove all existing depth labels.
502-
for k := range nsInst.Labels {
503-
if strings.HasSuffix(k, api.LabelTreeDepthSuffix) {
504-
delete(nsInst.Labels, k)
505-
}
530+
// There are no propagated objects in external namespaces so we don't need to notify the caller
531+
// that any propagation-relevant labels have changed.
532+
return false
506533
}
507534

508-
// Look for all ancestors. Stop as soon as we find a namespaces that has a critical condition in
509-
// the forest (note that AncestorHaltActivities is never included in the forest). This should handle orphans
510-
// and cycles.
535+
// Set all managed and tree labels, starting from the current namespace and going up through the
536+
// hierarchy.
511537
curNS := ns
512538
depth := 0
513539
for curNS != nil {
514-
l := curNS.Name() + api.LabelTreeDepthSuffix
515-
metadata.SetLabel(nsInst, l, strconv.Itoa(depth))
516-
if curNS.IsHalted() {
517-
break
540+
// Set the tree label from this layer of hierarchy
541+
metadata.SetLabel(nsInst, curNS.Name()+api.LabelTreeDepthSuffix, strconv.Itoa(depth))
542+
543+
// Add any managed labels. TODO: add conditions for conflicts.
544+
for k, v := range curNS.ManagedLabels {
545+
metadata.SetLabel(nsInst, k, v)
518546
}
519547

520548
// If the root is an external namespace, add all its external tree labels too.
@@ -524,21 +552,83 @@ func (r *Reconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, n
524552
for k, v := range curNS.GetTreeLabels() {
525553
metadata.SetLabel(nsInst, k, strconv.Itoa(depth+v))
526554
}
555+
556+
// Note that it's impossible to have an external namespace as a non-root (enforced elsewhere)
557+
// so technically we don't need to break out of the loop here. But I find it cleaner.
527558
break
528559
}
529560

561+
// Stop if this namespace is halted, which could indicate a cycle or orphan.
562+
if curNS.IsHalted() {
563+
break
564+
}
530565
curNS = curNS.Parent()
531566
depth++
532567
}
533-
// Update the labels in the forest so that we can quickly access the labels and
534-
// compare if they match the given selector
568+
569+
// Update the labels in the forest so that they can be used in object propagation. If they've
570+
// changed, return true so that all propagated objects in this namespace can be compared to its
571+
// new labels.
535572
if ns.SetLabels(nsInst.Labels) {
536573
log.Info("Namespace managed and tree labels have been updated")
537574
return true
538575
}
539576
return false
540577
}
541578

579+
// Sync namespace managed annotations. This is mainly a simplified version of syncLabels with all
580+
// the tree stuff taken out.
581+
func (r *Reconciler) syncAnnotations(log logr.Logger, inst *api.HierarchyConfiguration, nsInst *corev1.Namespace, ns *forest.Namespace) {
582+
// Get a list of all managed annotations from the config object.
583+
managed := map[string]string{}
584+
for _, kvp := range inst.Spec.Annotations {
585+
managed[kvp.Name] = kvp.Value
586+
}
587+
588+
// External namespaces can also have managed annotations set directly. All other namespaces must
589+
// have these removed now.
590+
for k, v := range nsInst.Annotations {
591+
if config.ManagedNamespaceAnnotations[k] {
592+
if ns.IsExternal() {
593+
managed[k] = v
594+
} else {
595+
delete(nsInst.Labels, k)
596+
}
597+
}
598+
}
599+
600+
// Record all managed annotations in the forest. If they've changed, we need to enqueue all
601+
// descendants to propagate the changes.
602+
if !reflect.DeepEqual(ns.ManagedAnnotations, managed) {
603+
ns.ManagedAnnotations = managed
604+
r.enqueueAffected(log, "managed annotations have changed", ns.DescendantNames()...)
605+
}
606+
607+
// For external namespaces, we're done since HNC mainly doesn't manage the metadata of
608+
// external namespaces.
609+
if ns.IsExternal() {
610+
return
611+
}
612+
613+
// Set all managed annotations, starting from the current namespace and going up through the
614+
// hierarchy.
615+
curNS := ns
616+
depth := 0
617+
for curNS != nil {
618+
// Add any managed annotations. TODO: add conditions for conflicts.
619+
for k, v := range curNS.ManagedAnnotations {
620+
metadata.SetAnnotation(nsInst, k, v)
621+
}
622+
623+
// Stop if this namespace is halted, which could indicate a cycle or orphan.
624+
if curNS.IsHalted() {
625+
break
626+
}
627+
curNS = curNS.Parent()
628+
depth++
629+
}
630+
}
631+
542632
func (r *Reconciler) syncConditions(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace, wasHalted bool) {
543633
// If the halted status has changed, notify
544634
if ns.IsHalted() != wasHalted {

0 commit comments

Comments
 (0)