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

Commit 44c5da2

Browse files
committed
Decouple reconcilers from each other
Instead of having the HC reconciler directly enqueue other reconcilers, it now has a level of indirection through the forest. This allows the anchor and object reconcilers - and, soon, the Hierarchical Resource Quota reconcilers - to be notified of changes to the hierarchy, without the HC reconciler having to know about them. (This change was prompted by my work on HRQs, which I felt would overly complicate the HC reconciler). At the same time, I've removed the enqueues of the HNC Config reconciler. Instead of reconciling whenever there was a change, with a rate-limited maximum, it's now just on a periodic ticker. Finally, I used the SetupWithManager functions as constructors to take the initialization of certain fields out of internal/setup and put them into the per-feature packages. Tested: all e2e tests still work (I didn't try the ones that require repair but those should be unaffected). Also tested this with my HRQ changes and HRQ works correctly and fits in with minimal changes to the rest of HNC.
1 parent 459ab1d commit 44c5da2

File tree

15 files changed

+325
-347
lines changed

15 files changed

+325
-347
lines changed

cmd/manager/main.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ func startControllers(mgr ctrl.Manager, certsReady chan struct{}) {
289289
// certs are all in place.
290290
setupLog.Info("Waiting for certificate generation to complete")
291291
<-certsReady
292+
setupLog.Info("Certs ready")
292293

293294
if testLog {
294295
stats.StartLoggingActivity()
@@ -298,18 +299,12 @@ func startControllers(mgr ctrl.Manager, certsReady chan struct{}) {
298299
// other components.
299300
f := forest.NewForest()
300301

301-
// Create all validating and mutating admission controllers.
302-
if !noWebhooks {
303-
setupLog.Info("Registering validating webhook (won't work when running locally; use --no-webhooks)")
304-
setup.CreateWebhooks(mgr, f)
305-
}
306-
307-
// Create all reconciling controllers
308-
setupLog.Info("Creating controllers", "maxReconciles", maxReconciles)
309-
if err := setup.CreateReconcilers(mgr, f, maxReconciles, webhooksOnly, false); err != nil {
310-
setupLog.Error(err, "cannot create controllers")
311-
os.Exit(1)
302+
opts := setup.Options{
303+
NoWebhooks: noWebhooks,
304+
MaxReconciles: maxReconciles,
305+
ReadOnly: webhooksOnly,
312306
}
307+
setup.Create(setupLog, mgr, f, opts)
313308

314309
setupLog.Info("All controllers started; setup complete")
315310
}

internal/anchor/reconciler.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ type Reconciler struct {
4747

4848
Forest *forest.Forest
4949

50-
// Affected is a channel of event.GenericEvent (see "Watching Channels" in
50+
// affected is a channel of event.GenericEvent (see "Watching Channels" in
5151
// https://book-v1.book.kubebuilder.io/beyond_basics/controller_watches.html) that is used to
5252
// enqueue additional objects that need updating.
53-
Affected chan event.GenericEvent
53+
affected chan event.GenericEvent
5454

5555
// ReadOnly disables writebacks
5656
ReadOnly bool
@@ -319,16 +319,22 @@ func (r *Reconciler) updateState(log logr.Logger, inst *api.SubnamespaceAnchor,
319319
}
320320
}
321321

322-
// Enqueue enqueues a subnamespace anchor for later reconciliation. This occurs in a goroutine
323-
// so the caller doesn't block; since the reconciler is never garbage-collected, this is safe.
324-
func (r *Reconciler) Enqueue(log logr.Logger, nm, pnm, reason string) {
322+
// OnChangeNamespace enqueues a subnamespace anchor for later reconciliation. This occurs in a
323+
// goroutine so the caller doesn't block; since the reconciler is never garbage-collected, this is
324+
// safe.
325+
func (r *Reconciler) OnChangeNamespace(log logr.Logger, ns *forest.Namespace) {
326+
if ns == nil || !ns.IsSub {
327+
return
328+
}
329+
nm := ns.Name()
330+
pnm := ns.Parent().Name()
325331
go func() {
326332
// The watch handler doesn't care about anything except the metadata.
327333
inst := &api.SubnamespaceAnchor{}
328334
inst.ObjectMeta.Name = nm
329335
inst.ObjectMeta.Namespace = pnm
330-
log.V(1).Info("Enqueuing for reconciliation", "affected", pnm+"/"+nm, "reason", reason)
331-
r.Affected <- event.GenericEvent{Object: inst}
336+
log.V(1).Info("Enqueuing for reconciliation", "affected", pnm+"/"+nm)
337+
r.affected <- event.GenericEvent{Object: inst}
332338
}()
333339
}
334340

@@ -423,6 +429,8 @@ func (r *Reconciler) deleteNamespace(ctx context.Context, log logr.Logger, inst
423429
}
424430

425431
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
432+
r.affected = make(chan event.GenericEvent)
433+
426434
// Maps an subnamespace to its anchor in the parent namespace.
427435
nsMapFn := func(obj client.Object) []reconcile.Request {
428436
if obj.GetAnnotations()[api.SubnamespaceOf] == "" {
@@ -437,7 +445,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
437445
}
438446
return ctrl.NewControllerManagedBy(mgr).
439447
For(&api.SubnamespaceAnchor{}).
440-
Watches(&source.Channel{Source: r.Affected}, &handler.EnqueueRequestForObject{}).
448+
Watches(&source.Channel{Source: r.affected}, &handler.EnqueueRequestForObject{}).
441449
Watches(&source.Kind{Type: &corev1.Namespace{}}, handler.EnqueueRequestsFromMapFunc(nsMapFn)).
442450
Complete(r)
443451
}

internal/forest/forest.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ type Forest struct {
3030
// We can also move the lock out of the forest and pass it to all reconcilers that need the lock.
3131
// In that way, we don't need to put the list in the forest.
3232
types []TypeSyncer
33+
34+
// nsListeners is a list of listeners
35+
listeners []NamespaceListener
3336
}
3437

3538
type namedNamespaces map[string]*Namespace
3639

3740
// TypeSyncer syncs objects of a specific type. Reconcilers implement the interface so that they can be
3841
// called by the HierarchyReconciler if the hierarchy changes.
3942
type TypeSyncer interface {
40-
// SyncNamespace syncs objects of a namespace for a specific type.
41-
SyncNamespace(context.Context, logr.Logger, string) error
42-
4343
// Provides the GVK that is handled by the reconciler who implements the interface.
4444
GetGVK() schema.GroupVersionKind
4545

@@ -55,6 +55,12 @@ type TypeSyncer interface {
5555
GetNumPropagatedObjects() int
5656
}
5757

58+
// NamespaceListener has methods that get called whenever a namespace changes.
59+
type NamespaceListener interface {
60+
// OnChangeNamespace is called whenever a namespace changes.
61+
OnChangeNamespace(logr.Logger, *Namespace)
62+
}
63+
5864
func NewForest() *Forest {
5965
return &Forest{
6066
namespaces: namedNamespaces{},
@@ -148,3 +154,13 @@ func (f *Forest) GetTypeSyncers() []TypeSyncer {
148154
copy(types, f.types)
149155
return types
150156
}
157+
158+
func (f *Forest) AddListener(l NamespaceListener) {
159+
f.listeners = append(f.listeners, l)
160+
}
161+
162+
func (f *Forest) OnChangeNamespace(log logr.Logger, ns *Namespace) {
163+
for _, l := range f.listeners {
164+
l.OnChangeNamespace(log, ns)
165+
}
166+
}

internal/forest/namespace.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ func (ns *Namespace) Name() string {
7171

7272
// Parent returns a pointer to the parent namespace.
7373
func (ns *Namespace) Parent() *Namespace {
74+
if ns == nil {
75+
return nil
76+
}
7477
return ns.parent
7578
}
7679

internal/forest/namespaceobjects.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package forest
33
import (
44
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
55
"k8s.io/apimachinery/pkg/runtime/schema"
6+
"k8s.io/apimachinery/pkg/types"
67
)
78

89
// SetSourceObject updates or creates the source object in forest.namespace.
@@ -35,11 +36,11 @@ func (ns *Namespace) DeleteSourceObject(gvk schema.GroupVersionKind, nm string)
3536
}
3637
}
3738

38-
// GetSourceObjects returns all source objects in the namespace.
39-
func (ns *Namespace) GetSourceObjects(gvk schema.GroupVersionKind) []*unstructured.Unstructured {
40-
o := []*unstructured.Unstructured{}
39+
// GetSourceNames returns all source objects in the namespace.
40+
func (ns *Namespace) GetSourceNames(gvk schema.GroupVersionKind) []types.NamespacedName {
41+
o := []types.NamespacedName{}
4142
for _, obj := range ns.sourceObjects[gvk] {
42-
o = append(o, obj)
43+
o = append(o, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()})
4344
}
4445
return o
4546
}
@@ -50,10 +51,10 @@ func (ns *Namespace) GetNumSourceObjects(gvk schema.GroupVersionKind) int {
5051
return len(ns.sourceObjects[gvk])
5152
}
5253

53-
// GetAncestorSourceObjects returns all source objects with the specified name
54+
// GetAncestorSourceNames returns all source objects with the specified name
5455
// in the ancestors (including itself) from top down. If the name is not
5556
// specified, all the source objects in the ancestors will be returned.
56-
func (ns *Namespace) GetAncestorSourceObjects(gvk schema.GroupVersionKind, name string) []*unstructured.Unstructured {
57+
func (ns *Namespace) GetAncestorSourceNames(gvk schema.GroupVersionKind, name string) []types.NamespacedName {
5758
// The namespace could be nil when we use this function on "ns.Parent()" to
5859
// get the source objects of the ancestors excluding itself without caring if
5960
// the "ns.Parent()" is nil.
@@ -62,22 +63,22 @@ func (ns *Namespace) GetAncestorSourceObjects(gvk schema.GroupVersionKind, name
6263
}
6364

6465
// Get the source objects in the ancestors from top down.
65-
objs := []*unstructured.Unstructured{}
66-
ans := ns.AncestryNames()
67-
for _, n := range ans {
68-
nsObjs := ns.forest.Get(n).GetSourceObjects(gvk)
66+
allNNMs := []types.NamespacedName{}
67+
ancs := ns.AncestryNames()
68+
for _, anc := range ancs {
69+
nnms := ns.forest.Get(anc).GetSourceNames(gvk)
6970
if name == "" {
7071
// Get all the source objects if the name is not specified.
71-
objs = append(objs, ns.forest.Get(n).GetSourceObjects(gvk)...)
72+
allNNMs = append(allNNMs, nnms...)
7273
} else {
7374
// If a name is specified, return the matching objects.
74-
for _, o := range nsObjs {
75-
if o.GetName() == name {
76-
objs = append(objs, o)
75+
for _, o := range nnms {
76+
if o.Name == name {
77+
allNNMs = append(allNNMs, o)
7778
}
7879
}
7980
}
8081
}
8182

82-
return objs
83+
return allNNMs
8384
}

0 commit comments

Comments
 (0)