From a8abd386e2179d4ac24a5ef6fd1528e4a98daab2 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 9 Nov 2023 16:02:34 -0500 Subject: [PATCH] Allow access to known children in ChildSetReconciler#DesiredChildren Current known children can be obtained via RetrieveKnownChildren[ChildType](ctx). This can be used to keep existing children while stamping out new resources, or for garbage collecting resources based on some criteria. Return the children that should be kept and omit children to delete. Signed-off-by: Scott Andrews --- reconcilers/childset.go | 49 +++++++++++++++++++++++----- reconcilers/childset_test.go | 63 +++++++++++++++++++++++++++++++++++- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/reconcilers/childset.go b/reconcilers/childset.go index cc1465d..2020572 100644 --- a/reconcilers/childset.go +++ b/reconcilers/childset.go @@ -82,6 +82,11 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // by the OurChild method with a stable, unique identifier returned. The identifier is used to // correlate desired and actual child resources. // + // Current known children can be obtained via RetrieveKnownChildren[ChildType](ctx). This can + // be used to keep existing children while stamping out new resources, or for garbage + // collecting resources based on some criteria. Return the children that should be kept and + // omit children to delete. + // // To skip reconciliation of the child resources while still reflecting an existing child's // status on the reconciled resource, return OnlyReconcileChildStatus as an error. DesiredChildren func(ctx context.Context, resource Type) ([]ChildType, error) @@ -280,6 +285,20 @@ func (r *ChildSetReconciler[T, CT, CLT]) composeChildReconcilers(ctx context.Con c := RetrieveConfigOrDie(ctx) childIDs := sets.NewString() + + children := r.ChildListType.DeepCopyObject().(CLT) + ourChildren := []CT{} + if err := c.List(ctx, children, r.voidReconciler.listOptions(ctx, resource)...); err != nil { + return nil, err + } + for _, child := range extractItems[CT](children) { + if !r.voidReconciler.ourChild(resource, child) { + continue + } + ourChildren = append(ourChildren, child.DeepCopyObject().(CT)) + } + + ctx = stashKnownChildren(ctx, ourChildren) desiredChildren, desiredChildrenErr := r.DesiredChildren(ctx, resource) if desiredChildrenErr != nil && !errors.Is(desiredChildrenErr, OnlyReconcileChildStatus) { return nil, desiredChildrenErr @@ -298,14 +317,7 @@ func (r *ChildSetReconciler[T, CT, CLT]) composeChildReconcilers(ctx context.Con desiredChildByID[id] = child } - children := r.ChildListType.DeepCopyObject().(CLT) - if err := c.List(ctx, children, r.voidReconciler.listOptions(ctx, resource)...); err != nil { - return nil, err - } - for _, child := range extractItems[CT](children) { - if !r.voidReconciler.ourChild(resource, child) { - continue - } + for _, child := range ourChildren { id := r.IdentifyChild(child) childIDs.Insert(id) } @@ -371,3 +383,24 @@ func clearChildSetResult[T client.Object](ctx context.Context) ChildSetResult[T] } return ChildSetResult[T]{} } + +const knownChildrenStashKey StashKey = "reconciler-runtime:knownChildren" + +// RetrieveKnownChildren returns the children managed by current ChildSetReconciler. The known +// children can be returned from the DesiredChildren method to preserve existing children, or to +// mutate/delete an existing child. +// +// For example, a child stamper could be implemented by returning existing children from +// DesiredChildren and appending an addition child when a new resource should be created. Likewise +// existing children can be garbage collected by omitting a known child. +func RetrieveKnownChildren[T client.Object](ctx context.Context) []T { + value := ctx.Value(knownChildrenStashKey) + if result, ok := value.([]T); ok { + return result + } + return nil +} + +func stashKnownChildren[T client.Object](ctx context.Context, children []T) context.Context { + return context.WithValue(ctx, knownChildrenStashKey, children) +} diff --git a/reconcilers/childset_test.go b/reconcilers/childset_test.go index bbfd812..2f33e67 100644 --- a/reconcilers/childset_test.go +++ b/reconcilers/childset_test.go @@ -8,6 +8,7 @@ package reconcilers_test import ( "context" "fmt" + "sort" "testing" "time" @@ -99,7 +100,7 @@ func TestChildSetReconciler(t *testing.T) { }) configMapGreenGiven := configMapGreenCreate. MetadataDie(func(d *diemetav1.ObjectMetaDie) { - d.CreationTimestamp(now) + d.CreationTimestamp(metav1.NewTime(now.Add(-1 * time.Hour))) d.UID(types.UID("62af4b9a-767a-4f32-b62c-e4bccbfa8ef0")) }) @@ -179,6 +180,66 @@ func TestChildSetReconciler(t *testing.T) { }, }, }, + "preserve existing children": { + Resource: resourceReady. + StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("blue.foo", "bar") + d.AddField("green.foo", "bar") + }). + DieReleasePtr(), + GivenObjects: []client.Object{ + configMapBlueGiven.DieReleasePtr(), + configMapGreenGiven.DieReleasePtr(), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + r := defaultChildSetReconciler(c) + r.DesiredChildren = func(ctx context.Context, resource *resources.TestResource) ([]*corev1.ConfigMap, error) { + children := reconcilers.RetrieveKnownChildren[*corev1.ConfigMap](ctx) + return children, nil + } + return r + }, + }, + }, + "garbage collect all but oldest child": { + Resource: resourceReady. + StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("blue.foo", "bar") + d.AddField("green.foo", "bar") + }). + DieReleasePtr(), + GivenObjects: []client.Object{ + configMapBlueGiven.DieReleasePtr(), + configMapGreenGiven.DieReleasePtr(), + }, + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + r := defaultChildSetReconciler(c) + r.DesiredChildren = func(ctx context.Context, resource *resources.TestResource) ([]*corev1.ConfigMap, error) { + children := reconcilers.RetrieveKnownChildren[*corev1.ConfigMap](ctx) + sort.Slice(children, func(i, j int) bool { + iDate := children[i].CreationTimestamp + jDate := children[j].CreationTimestamp + return iDate.Before(&jDate) + }) + return children[0:1], nil + } + return r + }, + }, + ExpectResource: resourceReady. + StatusDie(func(d *dies.TestResourceStatusDie) { + d.AddField("green.foo", "bar") + }). + DieReleasePtr(), + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Deleted", "Deleted ConfigMap %q", configMapBlueGiven.GetName()), + }, + ExpectDeletes: []rtesting.DeleteRef{ + rtesting.NewDeleteRefFromObject(configMapBlueGiven, scheme), + }, + }, "ignores resources that are not ours": { Resource: resourceReady. StatusDie(func(d *dies.TestResourceStatusDie) {