Skip to content

Commit c3013a0

Browse files
committed
Simplify condition handling
While implementing managed labels, I found two problems with our condition handling: 1. They were using old terminology like "Critical Condition" instead of the newer "ActivitiesHalted". 2. Instead of identifying problems like cycles right away, we delayed their identification until the end of the syncing process. This means that code that depends on knowing about cycles - like setting tree labels - needed to be at the end of the sync process instead of in the middle where they belonged (and couldn't set conditions themselves). This change fixes both problems. Tested: all existing unit/integ tests; no change in functionality.
1 parent a4fe5ce commit c3013a0

File tree

4 files changed

+92
-95
lines changed

4 files changed

+92
-95
lines changed
Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,47 @@
11
package forest
22

33
import (
4+
"fmt"
5+
46
api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2"
57
)
68

7-
// HasLocalCritCondition returns if the namespace itself has any local critical conditions, ignoring
8-
// its ancestors. Any code with the "Crit" prefix is a critical condition.
9-
func (ns *Namespace) HasLocalCritCondition() bool {
9+
// IsHalted returns true if this namespace has an ActivitiesHalted condition for any reason *other*
10+
// than one of its ancestors also being halted.
11+
func (ns *Namespace) IsHalted() bool {
12+
if ns == nil {
13+
return false
14+
}
1015
for _, cond := range ns.conditions {
11-
if cond.Type == api.ConditionActivitiesHalted && cond.Reason != api.ReasonAncestor {
16+
if cond.Type == api.ConditionActivitiesHalted {
1217
return true
1318
}
1419
}
1520
return false
1621
}
1722

18-
// GetCritAncestor returns the name of the first ancestor with a critical condition, or the empty
19-
// string if there are no such ancestors. It *can* return the name of the current namespace.
20-
func (ns *Namespace) GetCritAncestor() string {
21-
if ns.HasLocalCritCondition() {
22-
return ns.name
23-
}
24-
if ns.Parent() == nil {
23+
// GetHaltedRoot returns the name of the lowest subtree that's halted for any reason *other* than a
24+
// higher ancestor being halted. This can be the name of the current namespace, or the empty string
25+
// if there are no halted ancestors.
26+
func (ns *Namespace) GetHaltedRoot() string {
27+
if ns == nil {
2528
return ""
2629
}
27-
return ns.Parent().GetCritAncestor()
30+
if ns.IsHalted() {
31+
return ns.name
32+
}
33+
return ns.Parent().GetHaltedRoot()
2834
}
2935

3036
// SetCondition adds a new condition to the current condition list.
37+
//
38+
// Any condition with ReasonAncestor is ignored; this is added dynamically when calling
39+
// Conditions().
3140
func (ns *Namespace) SetCondition(tp, reason, msg string) {
32-
oldCond := ns.conditions
33-
ns.conditions = append(oldCond, api.NewCondition(tp, reason, msg))
41+
if reason == api.ReasonAncestor {
42+
return
43+
}
44+
ns.conditions = append(ns.conditions, api.NewCondition(tp, reason, msg))
3445
}
3546

3647
// ClearConditions set conditions to nil.
@@ -40,5 +51,11 @@ func (ns *Namespace) ClearConditions() {
4051

4152
// Conditions returns a full list of the conditions in the namespace.
4253
func (ns *Namespace) Conditions() []api.Condition {
54+
if root := ns.GetHaltedRoot(); root != "" && root != ns.name {
55+
msg := fmt.Sprintf("Propagation paused in %q and its descendants due to ActivitiesHalted condition on ancestor %q", ns.name, root)
56+
ret := []api.Condition{api.NewCondition(api.ConditionActivitiesHalted, api.ReasonAncestor, msg)}
57+
return append(ret, ns.conditions...)
58+
}
59+
4360
return ns.conditions
4461
}

internal/hierarchyconfig/reconciler.go

Lines changed: 50 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -290,11 +290,15 @@ func (r *Reconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, i
290290
defer r.Forest.Unlock()
291291
ns := r.Forest.Get(inst.ObjectMeta.Namespace)
292292

293-
// Clear locally-set conditions in the forest; we'll re-add them if they're still relevant. But
294-
// first, record whether there were any critical ones since if this changes, we'll need to notify
293+
// Clear all conditions; we'll re-add them if they're still relevant. But first, record whether
294+
// this namespace (excluding ancestors) was halted, since if that changes, we'll need to notify
295295
// other namespaces.
296-
hadCrit := ns.HasLocalCritCondition()
296+
wasHalted := ns.IsHalted()
297297
ns.ClearConditions()
298+
// We can figure out this condition pretty easily...
299+
if deletingCRD {
300+
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonDeletingCRD, "The HierarchyConfiguration CRD is being deleted; all propagation is disabled.")
301+
}
298302

299303
// Set external tree labels in the forest if this is an external namespace.
300304
r.syncExternalNamespace(log, nsInst, ns)
@@ -306,6 +310,9 @@ func (r *Reconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, i
306310
r.syncParent(log, inst, ns)
307311
initial := r.markExisting(log, ns)
308312

313+
// Sync labels and annotations, now that the structure's been updated.
314+
nsCustomerLabelUpdated := r.syncTreeLabels(log, nsInst, ns)
315+
309316
// Sync other spec and spec-like info
310317
r.syncAnchors(log, ns, anms)
311318
if ns.UpdateAllowCascadingDeletion(inst.Spec.AllowCascadingDeletion) {
@@ -315,11 +322,9 @@ func (r *Reconciler) syncWithForest(log logr.Logger, nsInst *corev1.Namespace, i
315322

316323
// Sync the status
317324
inst.Status.Children = ns.ChildNames()
318-
r.syncConditions(log, inst, ns, deletingCRD, hadCrit)
319-
320-
// Sync the tree labels. This should go last since it can depend on the conditions.
321-
nsCustomerLabelUpdated := r.syncTreeLabels(log, nsInst, ns)
325+
r.syncConditions(log, inst, ns, wasHalted)
322326

327+
r.HNCConfigReconciler.Enqueue("namespace reconciled")
323328
return initial || nsCustomerLabelUpdated
324329
}
325330

@@ -431,6 +436,9 @@ func (r *Reconciler) markExisting(log logr.Logger, ns *forest.Namespace) bool {
431436
}
432437

433438
func (r *Reconciler) syncParent(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace) {
439+
// As soon as the structure has been updated, do a cycle check.
440+
defer r.setCycleCondition(log, ns)
441+
434442
if ns.IsExternal() {
435443
ns.SetParent(nil)
436444
return
@@ -459,10 +467,10 @@ func (r *Reconciler) syncParent(log logr.Logger, inst *api.HierarchyConfiguratio
459467
// Change the parent.
460468
ns.SetParent(curParent)
461469

462-
// Finally, enqueue all other namespaces that could be directly affected. The old and new parents
463-
// have just gained/lost a child, while the descendants need to have their tree labels updated and
464-
// their objects resynced. Note that it's fine if oldParent or curParent is nil - see
465-
// enqueueAffected for details.
470+
// Enqueue all other namespaces that could be directly affected. The old and new parents have just
471+
// gained/lost a child, while the descendants need to have their tree labels updated and their
472+
// objects resynced. Note that it's fine if oldParent or curParent is nil - see enqueueAffected
473+
// for details.
466474
//
467475
// If we've just created a cycle, all the members of that cycle will be listed as the descendants,
468476
// so enqueuing them will ensure that the conditions show up in all members of the cycle.
@@ -471,6 +479,17 @@ func (r *Reconciler) syncParent(log logr.Logger, inst *api.HierarchyConfiguratio
471479
r.enqueueAffected(log, "subtree root has changed", ns.DescendantNames()...)
472480
}
473481

482+
func (r *Reconciler) setCycleCondition(log logr.Logger, ns *forest.Namespace) {
483+
cycle := ns.CycleNames()
484+
if cycle == nil {
485+
return
486+
}
487+
488+
msg := fmt.Sprintf("Namespace is a member of the cycle: %s", strings.Join(cycle, " <- "))
489+
log.Info(msg)
490+
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonInCycle, msg)
491+
}
492+
474493
// syncAnchors updates the anchor list. If any anchor is created/deleted, it will enqueue
475494
// the child to update its SubnamespaceAnchorMissing condition. A modified anchor will appear
476495
// twice in the change list (one in deleted, one in created), both subnamespaces
@@ -498,27 +517,27 @@ func (r *Reconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, n
498517
// Look for all ancestors. Stop as soon as we find a namespaces that has a critical condition in
499518
// the forest (note that AncestorHaltActivities is never included in the forest). This should handle orphans
500519
// and cycles.
501-
anc := ns
520+
curNS := ns
502521
depth := 0
503-
for anc != nil {
504-
l := anc.Name() + api.LabelTreeDepthSuffix
522+
for curNS != nil {
523+
l := curNS.Name() + api.LabelTreeDepthSuffix
505524
metadata.SetLabel(nsInst, l, strconv.Itoa(depth))
506-
if anc.HasLocalCritCondition() {
525+
if curNS.IsHalted() {
507526
break
508527
}
509528

510529
// If the root is an external namespace, add all its external tree labels too.
511530
// Note it's impossible to have an external namespace as a non-root, which is
512531
// enforced by both admission controllers and the reconciler here.
513-
if anc.IsExternal() {
514-
for k, v := range anc.ExternalTreeLabels {
532+
if curNS.IsExternal() {
533+
for k, v := range curNS.ExternalTreeLabels {
515534
l = k + api.LabelTreeDepthSuffix
516535
metadata.SetLabel(nsInst, l, strconv.Itoa(depth+v))
517536
}
518537
break
519538
}
520539

521-
anc = anc.Parent()
540+
curNS = curNS.Parent()
522541
depth++
523542
}
524543
// Update the labels in the forest so that we can quickly access the labels and
@@ -530,61 +549,22 @@ func (r *Reconciler) syncTreeLabels(log logr.Logger, nsInst *corev1.Namespace, n
530549
return false
531550
}
532551

533-
func (r *Reconciler) syncConditions(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace, deletingCRD, hadCrit bool) {
534-
// Sync critical conditions after all locally-set conditions are updated.
535-
r.syncCritConditions(log, ns, deletingCRD, hadCrit)
552+
func (r *Reconciler) syncConditions(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace, wasHalted bool) {
553+
// If the halted status has changed, notify
554+
if ns.IsHalted() != wasHalted {
555+
msg := ""
556+
if wasHalted {
557+
log.Info("ActivitiesHalted condition removed")
558+
msg = "removed"
559+
} else {
560+
log.Info("Setting ActivitiesHalted on namespace", "conditions", ns.Conditions())
561+
msg = "added"
562+
}
563+
r.enqueueAffected(log, "descendant of a namespace with ActivitiesHalted "+msg, ns.DescendantNames()...)
564+
}
536565

537566
// Convert and pass in-memory conditions to HierarchyConfiguration object.
538567
inst.Status.Conditions = ns.Conditions()
539-
setCritAncestorCondition(log, inst, ns)
540-
r.HNCConfigReconciler.Enqueue("namespace reconciled")
541-
}
542-
543-
// syncCritConditions enqueues the children of a namespace if the existing critical conditions in the
544-
// namespace are gone or critical conditions are newly found.
545-
func (r *Reconciler) syncCritConditions(log logr.Logger, ns *forest.Namespace, deletingCRD, hadCrit bool) {
546-
// If we're in a cycle, determine that now
547-
if cycle := ns.CycleNames(); cycle != nil {
548-
msg := fmt.Sprintf("Namespace is a member of the cycle: %s", strings.Join(cycle, " <- "))
549-
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonInCycle, msg)
550-
}
551-
552-
if deletingCRD {
553-
ns.SetCondition(api.ConditionActivitiesHalted, api.ReasonDeletingCRD, "The HierarchyConfiguration CRD is being deleted; all syncing is disabled.")
554-
}
555-
556-
// Early exit if nothing's changed and there's no need to enqueue relatives.
557-
if hadCrit == ns.HasLocalCritCondition() {
558-
return
559-
}
560-
561-
msg := ""
562-
if hadCrit {
563-
log.Info("ActivitiesHalted condition removed")
564-
msg = "removed"
565-
} else {
566-
log.Info("Setting ActivitiesHalted on namespace", "conditions", ns.Conditions())
567-
msg = "added"
568-
}
569-
r.enqueueAffected(log, "descendant of a namespace with ActivitiesHalted "+msg, ns.DescendantNames()...)
570-
}
571-
572-
func setCritAncestorCondition(log logr.Logger, inst *api.HierarchyConfiguration, ns *forest.Namespace) {
573-
if ns.HasLocalCritCondition() {
574-
return
575-
}
576-
ans := ns.Parent()
577-
for ans != nil {
578-
if !ans.HasLocalCritCondition() {
579-
ans = ans.Parent()
580-
continue
581-
}
582-
log.Info("Ancestor has a ActivitiesHalted condition", "ancestor", ans.Name())
583-
msg := fmt.Sprintf("Propagation paused in the subtree of %q due to ActivitiesHalted condition", ans.Name())
584-
condition := api.NewCondition(api.ConditionActivitiesHalted, api.ReasonAncestor, msg)
585-
inst.Status.Conditions = append(inst.Status.Conditions, condition)
586-
return
587-
}
588568
}
589569

590570
// enqueueAffected enqueues all affected namespaces for later reconciliation. This occurs in a

internal/hierarchyconfig/validator.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,11 @@ func (v *Validator) checkNS(ns *forest.Namespace) admission.Response {
175175
return deny(metav1.StatusReasonServiceUnavailable, msg)
176176
}
177177

178-
// Deny the request if the namespace has a critical ancestor - but not if it's critical itself,
179-
// since we may be trying to resolve the critical condition.
180-
critAnc := ns.GetCritAncestor()
181-
if critAnc != "" && critAnc != ns.Name() {
182-
msg := fmt.Sprintf("The ancestor %q of namespace %q has a critical condition, which must be resolved before any changes can be made to the hierarchy configuration.", critAnc, ns.Name())
178+
// Deny the request if the namespace has a halted root - but not if it's halted itself, since we
179+
// may be trying to resolve the halted condition.
180+
haltedRoot := ns.GetHaltedRoot()
181+
if haltedRoot != "" && haltedRoot != ns.Name() {
182+
msg := fmt.Sprintf("The ancestor %q of namespace %q has a critical condition, which must be resolved before any changes can be made to the hierarchy configuration.", haltedRoot, ns.Name())
183183
return deny(metav1.StatusReasonForbidden, msg)
184184
}
185185

internal/objects/reconciler.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ func (r *Reconciler) syncWithForest(ctx context.Context, log logr.Logger, inst *
256256
// what we'd _like_ to do. We still needed to sync the forest since we want to know when objects
257257
// are added and removed, so we can sync them properly if the critical condition is resolved, but
258258
// don't do anything else for now.
259-
if ca := r.Forest.Get(inst.GetNamespace()).GetCritAncestor(); ca != "" {
260-
log.Info("Namespace has 'ActivitiesHalted' condition; will not touch propagated object", "affectedNamespace", ca, "suppressedAction", action)
259+
if halted := r.Forest.Get(inst.GetNamespace()).GetHaltedRoot(); halted != "" {
260+
log.Info("Namespace has 'ActivitiesHalted' condition; will not touch propagated object", "haltedRoot", halted, "suppressedAction", action)
261261
return actionNop, nil
262262
}
263263

@@ -478,10 +478,10 @@ func cleanSource(src *unstructured.Unstructured) *unstructured.Unstructured {
478478

479479
func (r *Reconciler) enqueueDescendants(ctx context.Context, log logr.Logger, src *unstructured.Unstructured, reason string) {
480480
sns := r.Forest.Get(src.GetNamespace())
481-
if ca := sns.GetCritAncestor(); ca != "" {
482-
// There's no point enqueuing anything if the source namespace has a crit condition since we'll
483-
// just skip any action anyway.
484-
log.V(1).Info("Will not enqueue descendants due to crit ancestor", "critAncestor", ca, "oldReason", reason)
481+
if halted := sns.GetHaltedRoot(); halted != "" {
482+
// There's no point enqueuing anything if the source namespace is halted since we'll just skip
483+
// any action anyway.
484+
log.V(1).Info("Will not enqueue descendants since activities are halted", "haltedRoot", halted, "oldReason", reason)
485485
return
486486
}
487487
log.V(1).Info("Enqueuing descendant objects", "reason", reason)

0 commit comments

Comments
 (0)