Skip to content

Commit

Permalink
Allow access to known children in ChildSetReconciler#DesiredChildren (#…
Browse files Browse the repository at this point in the history
…458)

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 <andrewssc@vmware.com>
  • Loading branch information
scothis committed Nov 21, 2023
1 parent e002cf4 commit e22ffba
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 9 deletions.
49 changes: 41 additions & 8 deletions reconcilers/childset.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
63 changes: 62 additions & 1 deletion reconcilers/childset_test.go
Expand Up @@ -8,6 +8,7 @@ package reconcilers_test
import (
"context"
"fmt"
"sort"
"testing"
"time"

Expand Down Expand Up @@ -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"))
})

Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit e22ffba

Please sign in to comment.