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

Commit bad9db5

Browse files
authored
Merge pull request #119 from adrianludwin/cond-simplify
Simplify condition handling
2 parents 687e545 + c3013a0 commit bad9db5

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)