diff --git a/README.md b/README.md index f628725..71dcfca 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Within an existing Kubebuilder or controller-runtime project, reconcilers.io may - [Tracker](#tracker) - [Status](#status) - [Finalizers](#finalizers) - - [ResourceManager](#resourcemanager) + - [ObjectManager](#objectmanager) + - [UpdatingObjectManager](#updatingobjectmanager) - [Time](#time) - [Breaking Changes](#breaking-changes) - [Current Deprecations](#current-deprecations) @@ -1119,17 +1120,26 @@ A minimal test case for a sub reconciler that adds a finalizer may look like: ... ``` -### ResourceManager +### ObjectManager -The [`ResourceManager`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#ResourceManager) provides a means to manage a single resource by synchronizing the current and desired state. The resource will be created if it does not exist, deleted if no longer desired and updated when semantically different. The same resource manager should be reused to manage multiple resources and must be reused when managing the same resource over time in order to take full effect. This utility is used by the [ChildReconciler](#childreconciler) and [AggregateReconciler](#aggregatereconciler). +The [`ObjectManager`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#ObjectManager) is an interface providing a means to manage a single resource by synchronizing the current and desired state. The resource will be created if it does not exist, deleted if no longer desired and updated when semantically different. The same resource manager should be reused to manage multiple resources and must be reused when managing the same resource over time in order to take full effect. This utility is used by the [ChildReconciler](#childreconciler), [ChildSetReconciler](#childsetreconciler) and [AggregateReconciler](#aggregatereconciler). + +The interface is designed to allow for multiple synchronization strategies to be used. The `Manage(ctx context.Context, resource, actual, desired client.Object) (client.Object, error)` method take three objects and returns another object: - `resource` is the reconciled resource, events, tracks and finalizer are against this object. May be an object of any underlaying type. - `actual` the resource that exists on the API Server. Must be compatible with the `Type`. -- `desired` the resoruce that should exist on the API Server after this call. Must be compatible with the `Type`. +- `desired` the resource that should exist on the API Server after this call. Must be compatible with the `Type`. - the returned object is the value as persisted by the API Server. -Internally, a mutations made to the resoruce at admission time (like defaults applied by a mutating webhook) are captured and reapplied to the desired state before checking if an update is needed. This reduces requests that are functionally a no-op but create churn on the API Server. The mutation cache is defensive and fails open to make an API request. +Use a provided ObjectManager or define a custom strategy to change specific behavior or employ entirely new approaches to sync state to the API Server. + + +#### UpdatingObjectManager + +The [`UpdatingObjectManager`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#UpdatingObjectManager) (previously [`ResourceManager`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#ResourceManager)) uses the `client.Client#{Create, Update, Delete}` methods to synchronize state to the API Server. + +Internally, a mutations made to the resource at admission time (like defaults applied by a mutating webhook) are captured and reapplied to the desired state before checking if an update is needed. This reduces requests that are functionally a no-op but create churn on the API Server. The mutation cache is defensive and fails open to make an API request. If configured, a [finalizer](#finalizers) can be managed on the resource which will be added before create/udpate and removed after sucessful delete. @@ -1153,6 +1163,10 @@ reconciler.io runtime is rapidly evolving. While we strive for API compatability Backwards support may be removed in a future release, users are encouraged to migrate. +- `ResourceManager` is deprecated in favor of `ObjectManager` for a generic type, or `UpdatingObjectManager`. +- `AggregateReconciler.{HarmonizeImmutableFields, MergeBeforeUpdate, Sanitize}` are deprecated in favor of `AggregateReconciler.AggregateObjectManager`. +- `ChildReconciler.{Finalizer, HarmonizeImmutableFields, MergeBeforeUpdate, Sanitize, SetResourceManager}` are deprecated in favor of `ChildReconciler.ChildObjectManager`. +- `ChildSetReconciler.{HarmonizeImmutableFields, MergeBeforeUpdate, Sanitize}` are deprecated in favor of `ChildSetReconciler.ChildObjectManager`. - status `InitializeConditions()` is deprecated in favor of `InitializeConditions(context.Context)`. - `ConditionSet#Manage` is deprecated in favor of `ConditionSet#ManageWithContext`. - `HaltSubReconcilers` is deprecated in favor of `ErrHaltSubReconcilers`. diff --git a/reconcilers/aggregate.go b/reconcilers/aggregate.go index cb9d5ec..864e06c 100644 --- a/reconcilers/aggregate.go +++ b/reconcilers/aggregate.go @@ -85,6 +85,11 @@ type AggregateReconciler[Type client.Object] struct { // +optional DesiredResource func(ctx context.Context, resource Type) (Type, error) + // AggregateObjectManager synchronizes the aggregated resource with the API Server. + AggregateObjectManager ObjectManager[Type] + + // Deprecated use AggregateObjectManager instead. Ignored when AggregateObjectManager is defined. + // // HarmonizeImmutableFields allows fields that are immutable on the current // object to be copied to the desired object in order to avoid creating // updates which are guaranteed to fail. @@ -92,11 +97,15 @@ type AggregateReconciler[Type client.Object] struct { // +optional HarmonizeImmutableFields func(current, desired Type) + // Deprecated use AggregateObjectManager instead. Ignored when AggregateObjectManager is defined. + // // MergeBeforeUpdate copies desired fields on to the current object before // calling update. Typically fields to copy are the Spec, Labels and // Annotations. MergeBeforeUpdate func(current, desired Type) + // Deprecated use AggregateObjectManager instead. Ignored when AggregateObjectManager is defined. + // // Sanitize is called with an object before logging the value. Any value may // be returned. A meaningful subset of the resource is typically returned, // like the Spec. @@ -122,8 +131,6 @@ type AggregateReconciler[Type client.Object] struct { Config Config - // stamp manages the lifecycle of the aggregated resource. - stamp *ResourceManager[Type] lazyInit sync.Once } @@ -155,13 +162,16 @@ func (r *AggregateReconciler[T]) init() { } } - r.stamp = &ResourceManager[T]{ - Name: r.Name, - Type: r.Type, + if r.AggregateObjectManager == nil { + // Deprecated compatibility fallback + r.AggregateObjectManager = &UpdatingObjectManager[T]{ + Name: r.Name, + Type: r.Type, - HarmonizeImmutableFields: r.HarmonizeImmutableFields, - MergeBeforeUpdate: r.MergeBeforeUpdate, - Sanitize: r.Sanitize, + HarmonizeImmutableFields: r.HarmonizeImmutableFields, + MergeBeforeUpdate: r.MergeBeforeUpdate, + Sanitize: r.Sanitize, + } } }) } @@ -213,7 +223,7 @@ func (r *AggregateReconciler[T]) SetupWithManagerYieldingController(ctx context. if err := r.Reconciler.SetupWithManager(ctx, mgr, bldr); err != nil { return nil, err } - if err := r.stamp.Setup(ctx); err != nil { + if err := r.AggregateObjectManager.SetupWithManager(ctx, mgr, bldr); err != nil { return nil, err } return bldr.Build(r) @@ -309,7 +319,7 @@ func (r *AggregateReconciler[T]) reconcile(ctx context.Context, req Request) (Re if err != nil { return result, err } - _, err = r.stamp.Manage(ctx, resource, resource, desired) + _, err = r.AggregateObjectManager.Manage(ctx, resource, resource, desired) return result, err } diff --git a/reconcilers/child.go b/reconcilers/child.go index 18fb293..a7e34f7 100644 --- a/reconcilers/child.go +++ b/reconcilers/child.go @@ -76,6 +76,8 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL // +optional ChildListType ChildListType + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // Finalizer is set on the reconciled resource before a child resource is created, and cleared // after a child resource is deleted. The value must be unique to this specific reconciler // instance and not shared. Reusing a value may result in orphaned resources when the @@ -119,6 +121,11 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL // may grow, implementations should be defensive rather than assuming the error type. ReflectChildStatusOnParent func(ctx context.Context, parent Type, child ChildType, err error) + // ChildObjectManager synchronizes the desired child state to the API Server. + ChildObjectManager ObjectManager[ChildType] + + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // HarmonizeImmutableFields allows fields that are immutable on the current // object to be copied to the desired object in order to avoid creating // updates which are guaranteed to fail. @@ -126,6 +133,8 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL // +optional HarmonizeImmutableFields func(current, desired ChildType) + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // MergeBeforeUpdate copies desired fields on to the current object before // calling update. Typically fields to copy are the Spec, Labels and // Annotations. @@ -158,6 +167,8 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL // +optional OurChild func(resource Type, child ChildType) bool + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // Sanitize is called with an object before logging the value. Any value may // be returned. A meaningful subset of the resource is typically returned, // like the Spec. @@ -165,7 +176,6 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL // +optional Sanitize func(child ChildType) interface{} - stamp *ResourceManager[ChildType] lazyInit sync.Once } @@ -182,11 +192,9 @@ func (r *ChildReconciler[T, CT, CLT]) init() { if r.Name == "" { r.Name = fmt.Sprintf("%sChildReconciler", typeName(r.ChildType)) } - if r.Sanitize == nil { - r.Sanitize = func(child CT) interface{} { return child } - } - if r.stamp == nil { - r.stamp = &ResourceManager[CT]{ + if r.ChildObjectManager == nil { + // Deprecated compatibility fallback + r.ChildObjectManager = &UpdatingObjectManager[CT]{ Name: r.Name, Type: r.ChildType, Finalizer: r.Finalizer, @@ -213,16 +221,21 @@ func (r *ChildReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr return err } - if r.SkipOwnerReference { - bldr.Watches(r.ChildType, EnqueueTracked(ctx)) - } else { + if !r.SkipOwnerReference { bldr.Owns(r.ChildType) } - if r.Setup == nil { - return nil + if err := r.ChildObjectManager.SetupWithManager(ctx, mgr, bldr); err != nil { + return err } - return r.Setup(ctx, mgr, bldr) + + if r.Setup != nil { + if err := r.Setup(ctx, mgr, bldr); err != nil { + return err + } + } + + return nil } func (r *ChildReconciler[T, CT, CLT]) validate(ctx context.Context) error { @@ -251,19 +264,23 @@ func (r *ChildReconciler[T, CT, CLT]) validate(ctx context.Context) error { return fmt.Errorf("ChildReconciler %q must implement ListOptions since owner references are not used", r.Name) } - // require MergeBeforeUpdate - if r.MergeBeforeUpdate == nil { - return fmt.Errorf("ChildReconciler %q must implement MergeBeforeUpdate", r.Name) + // Deprecated fallback validation + if m, ok := r.ChildObjectManager.(*UpdatingObjectManager[CT]); ok { + // require MergeBeforeUpdate + if m.MergeBeforeUpdate == nil { + return fmt.Errorf("ChildReconciler %q must implement MergeBeforeUpdate", r.Name) + } } return nil } -func (r *ChildReconciler[T, CT, CLT]) SetResourceManager(rm *ResourceManager[CT]) { - if r.stamp != nil { +// Deprecated use ChildObjectManager instead +func (r *ChildReconciler[T, CT, CLT]) SetResourceManager(rm ObjectManager[CT]) { + if r.ChildObjectManager != nil { panic(fmt.Errorf("cannot call SetResourceManager after a resource manager is defined")) } - r.stamp = rm + r.ChildObjectManager = rm } func (r *ChildReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T) (Result, error) { @@ -364,7 +381,7 @@ func (r *ChildReconciler[T, CT, CLT]) reconcile(ctx context.Context, resource T) } // create/update/delete desired child - return r.stamp.Manage(ctx, resource, actual, desired) + return r.ChildObjectManager.Manage(ctx, resource, actual, desired) } func (r *ChildReconciler[T, CT, CLT]) desiredChild(ctx context.Context, resource T) (CT, error) { diff --git a/reconcilers/childset.go b/reconcilers/childset.go index 8726a70..435c40a 100644 --- a/reconcilers/childset.go +++ b/reconcilers/childset.go @@ -102,6 +102,9 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // status on the reconciled resource, return OnlyReconcileChildStatus as an error. DesiredChildren func(ctx context.Context, resource Type) ([]ChildType, error) + // ChildObjectManager synchronizes the desired child state to the API Server. + ChildObjectManager ObjectManager[ChildType] + // ReflectChildrenStatusOnParent updates the reconciled resource's status with values from the // child reconciliations. Select types of errors are captured, including: // - apierrs.IsAlreadyExists @@ -114,6 +117,8 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // reconciled, (sorted by identifier). ReflectChildrenStatusOnParent func(ctx context.Context, parent Type, result ChildSetResult[ChildType]) + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // HarmonizeImmutableFields allows fields that are immutable on the current // object to be copied to the desired object in order to avoid creating // updates which are guaranteed to fail. @@ -121,6 +126,8 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // +optional HarmonizeImmutableFields func(current, desired ChildType) + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // MergeBeforeUpdate copies desired fields on to the current object before // calling update. Typically fields to copy are the Spec, Labels and // Annotations. @@ -161,6 +168,8 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // Non-deterministic IDs will result in the rapid deletion and creation of child resources. IdentifyChild func(child ChildType) string + // Deprecated use ChildObjectManager instead. Ignored when ChildObjectManager is defined. + // // Sanitize is called with an object before logging the value. Any value may // be returned. A meaningful subset of the resource is typically returned, // like the Spec. @@ -168,7 +177,6 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje // +optional Sanitize func(child ChildType) interface{} - stamp *ResourceManager[ChildType] lazyInit sync.Once voidReconciler *ChildReconciler[Type, ChildType, ChildListType] } @@ -186,13 +194,16 @@ func (r *ChildSetReconciler[T, CT, CLT]) init() { if r.Name == "" { r.Name = fmt.Sprintf("%sChildSetReconciler", typeName(r.ChildType)) } - r.stamp = &ResourceManager[CT]{ - Name: r.Name, - Type: r.ChildType, - TrackDesired: r.SkipOwnerReference, - HarmonizeImmutableFields: r.HarmonizeImmutableFields, - MergeBeforeUpdate: r.MergeBeforeUpdate, - Sanitize: r.Sanitize, + if r.ChildObjectManager == nil { + // Deprecated compatibility fallback + r.ChildObjectManager = &UpdatingObjectManager[CT]{ + Name: r.Name, + Type: r.ChildType, + TrackDesired: r.SkipOwnerReference, + HarmonizeImmutableFields: r.HarmonizeImmutableFields, + MergeBeforeUpdate: r.MergeBeforeUpdate, + Sanitize: r.Sanitize, + } } r.voidReconciler = r.childReconcilerFor(nilCT, nil, "", true) }) @@ -212,14 +223,21 @@ func (r *ChildSetReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, m return err } + if err := r.ChildObjectManager.SetupWithManager(ctx, mgr, bldr); err != nil { + return err + } + if err := r.voidReconciler.SetupWithManager(ctx, mgr, bldr); err != nil { return err } - if r.Setup == nil { - return nil + if r.Setup != nil { + if err := r.Setup(ctx, mgr, bldr); err != nil { + return err + } } - return r.Setup(ctx, mgr, bldr) + + return nil } func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredErr error, id string, void bool) *ChildReconciler[T, CT, CLT] { @@ -231,6 +249,7 @@ func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredE DesiredChild: func(ctx context.Context, resource T) (CT, error) { return desired, desiredErr }, + ChildObjectManager: r.ChildObjectManager, ReflectChildStatusOnParent: func(ctx context.Context, parent T, child CT, err error) { result := childSetResultStasher[CT]().RetrieveOrEmpty(ctx) result.Children = append(result.Children, ChildSetPartialResult[CT]{ @@ -240,16 +259,13 @@ func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredE }) childSetResultStasher[CT]().Store(ctx, result) }, - HarmonizeImmutableFields: r.HarmonizeImmutableFields, - MergeBeforeUpdate: r.MergeBeforeUpdate, - ListOptions: r.ListOptions, + ListOptions: r.ListOptions, OurChild: func(resource T, child CT) bool { if r.OurChild != nil && !r.OurChild(resource, child) { return false } return void || id == r.IdentifyChild(child) }, - Sanitize: r.Sanitize, } } @@ -356,7 +372,6 @@ func (r *ChildSetReconciler[T, CT, CLT]) composeChildReconcilers(ctx context.Con for _, id := range childIDs.List() { child := desiredChildByID[id] cr := r.childReconcilerFor(child, desiredChildrenErr, id, false) - cr.SetResourceManager(r.stamp) sequence = append(sequence, cr) } diff --git a/reconcilers/resourcemanager.go b/reconcilers/objectmanager.go similarity index 68% rename from reconcilers/resourcemanager.go rename to reconcilers/objectmanager.go index 6e503fe..fc79347 100644 --- a/reconcilers/resourcemanager.go +++ b/reconcilers/objectmanager.go @@ -31,14 +31,24 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/util/cache" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "reconciler.io/runtime/internal" ) -// ResourceManager compares the actual and desired resources to create/update/delete as desired. -type ResourceManager[Type client.Object] struct { - // Name used to identify this reconciler. Defaults to `{Type}ResourceManager`. Ideally +type ObjectManager[Type client.Object] interface { + SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error + Manage(ctx context.Context, resource client.Object, actual, desired Type) (Type, error) +} + +var _ ObjectManager[client.Object] = (*UpdatingObjectManager[client.Object])(nil) +var _ ObjectManager[client.Object] = (*ResourceManager[client.Object])(nil) + +// UpdatingObjectManager compares the actual and desired resources to create/update/delete as desired. +type UpdatingObjectManager[Type client.Object] struct { + // Name used to identify this reconciler. Defaults to `{Type}UpdatingObjectManager`. Ideally // unique, but not required to be so. // // +optional @@ -92,28 +102,41 @@ type ResourceManager[Type client.Object] struct { lazyInit sync.Once } -func (r *ResourceManager[T]) init() { +func (r *UpdatingObjectManager[T]) init() { r.lazyInit.Do(func() { if internal.IsNil(r.Type) { var nilT T r.Type = newEmpty(nilT).(T) } if r.Name == "" { - r.Name = fmt.Sprintf("%sResourceManager", typeName(r.Type)) + r.Name = fmt.Sprintf("%sUpdatingObjectManager", typeName(r.Type)) } r.mutationCache = cache.NewExpiring() }) } -func (r *ResourceManager[T]) Setup(ctx context.Context) error { +// Deprecated call SetupWithManager instead +func (r *UpdatingObjectManager[T]) Setup(ctx context.Context) error { r.init() return r.validate(ctx) } -func (r *ResourceManager[T]) validate(ctx context.Context) error { +func (r *UpdatingObjectManager[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { + if err := r.Setup(ctx); err != nil { + return err + } + + if r.TrackDesired { + bldr.Watches(r.Type, EnqueueTracked(ctx)) + } + + return nil +} + +func (r *UpdatingObjectManager[T]) validate(ctx context.Context) error { // require MergeBeforeUpdate if r.MergeBeforeUpdate == nil { - return fmt.Errorf("ResourceManager %q must define MergeBeforeUpdate", r.Name) + return fmt.Errorf("UpdatingObjectManager %q must define MergeBeforeUpdate", r.Name) } return nil @@ -122,7 +145,7 @@ func (r *ResourceManager[T]) validate(ctx context.Context) error { // Manage a specific resource to create/update/delete based on the actual and desired state. The // resource is the reconciled resource and used to record events for mutations. The actual and // desired objects represent the managed resource and must be compatible with the type field. -func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, actual, desired T) (T, error) { +func (r *UpdatingObjectManager[T]) Manage(ctx context.Context, resource client.Object, actual, desired T) (T, error) { r.init() var nilT T @@ -243,7 +266,7 @@ func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, return current, nil } -func (r *ResourceManager[T]) sanitize(resource T) interface{} { +func (r *UpdatingObjectManager[T]) sanitize(resource T) interface{} { if r.Sanitize == nil { return resource } @@ -308,3 +331,96 @@ func (p *Patch) Apply(rebase client.Object) error { replaceWithEmpty(rebase) return json.Unmarshal(patchedBytes, rebase) } + +// Deprecated ResourceManager use either ObjectManger for the generic type, or +// UpdatingObjectManager for a specific instance. +// +// The type is duplicated because generics are not supported in type aliases. +// See https://github.com/golang/go/issues/46477#issuecomment-852701491 +type ResourceManager[Type client.Object] struct { + // Name used to identify this reconciler. Defaults to `{Type}ResourceManager`. Ideally + // unique, but not required to be so. + // + // +optional + Name string + + // Type is the resource being created/updated/deleted by the reconciler. Required when the + // generic type is not a struct, or is unstructured. + // + // +optional + Type Type + + // Finalizer is set on the reconciled resource before a managed resource is created, and cleared + // after a managed resource is deleted. The value must be unique to this specific manager + // instance and not shared. Reusing a value may result in orphaned resources when the + // reconciled resource is deleted. + // + // Using a finalizer is encouraged when the Kubernetes garbage collector is unable to delete + // the child resource automatically, like when the reconciled resource and child are in different + // namespaces, scopes or clusters. + // + // +optional + Finalizer string + + // TrackDesired when true, the desired resource is tracked after creates, before + // updates, and on delete errors. + TrackDesired bool + + // HarmonizeImmutableFields allows fields that are immutable on the current + // object to be copied to the desired object in order to avoid creating + // updates which are guaranteed to fail. + // + // +optional + HarmonizeImmutableFields func(current, desired Type) + + // MergeBeforeUpdate copies desired fields on to the current object before + // calling update. Typically fields to copy are the Spec, Labels and + // Annotations. + MergeBeforeUpdate func(current, desired Type) + + // Sanitize is called with an object before logging the value. Any value may + // be returned. A meaningful subset of the resource is typically returned, + // like the Spec. + // + // +optional + Sanitize func(child Type) interface{} + + internal *UpdatingObjectManager[Type] + lazyInit sync.Once +} + +func (r *ResourceManager[T]) init() { + r.lazyInit.Do(func() { + if internal.IsNil(r.Type) { + var nilT T + r.Type = newEmpty(nilT).(T) + } + if r.Name == "" { + r.Name = fmt.Sprintf("%sResourceManager", typeName(r.Type)) + } + r.internal = &UpdatingObjectManager[T]{ + Name: r.Name, + Type: r.Type, + Finalizer: r.Finalizer, + TrackDesired: r.TrackDesired, + HarmonizeImmutableFields: r.HarmonizeImmutableFields, + MergeBeforeUpdate: r.MergeBeforeUpdate, + Sanitize: r.Sanitize, + } + }) +} + +func (r *ResourceManager[T]) Setup(ctx context.Context) error { + r.init() + return r.internal.Setup(ctx) +} + +func (r *ResourceManager[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { + r.init() + return r.internal.SetupWithManager(ctx, mgr, bldr) +} + +func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, actual, desired T) (T, error) { + r.init() + return r.internal.Manage(ctx, resource, actual, desired) +} diff --git a/reconcilers/resourcemanager_test.go b/reconcilers/objectmanager_test.go similarity index 100% rename from reconcilers/resourcemanager_test.go rename to reconcilers/objectmanager_test.go diff --git a/reconcilers/validate_test.go b/reconcilers/validate_test.go index 16bb49a..f72faeb 100644 --- a/reconcilers/validate_test.go +++ b/reconcilers/validate_test.go @@ -202,10 +202,9 @@ func TestAggregateReconciler_validate(t *testing.T) { { name: "valid", reconciler: &AggregateReconciler[*resources.TestResource]{ - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence[*resources.TestResource]{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, + Type: &resources.TestResource{}, + Request: req, + Reconciler: Sequence[*resources.TestResource]{}, }, }, { @@ -213,9 +212,8 @@ func TestAggregateReconciler_validate(t *testing.T) { reconciler: &AggregateReconciler[*resources.TestResource]{ Name: "Type missing", // Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence[*resources.TestResource]{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, + Request: req, + Reconciler: Sequence[*resources.TestResource]{}, }, }, { @@ -224,8 +222,7 @@ func TestAggregateReconciler_validate(t *testing.T) { Name: "Request missing", Type: &resources.TestResource{}, // Request: req, - Reconciler: Sequence[*resources.TestResource]{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, + Reconciler: Sequence[*resources.TestResource]{}, }, shouldErr: `AggregateReconciler "Request missing" must define Request`, }, @@ -236,17 +233,15 @@ func TestAggregateReconciler_validate(t *testing.T) { Type: &resources.TestResource{}, Request: req, // Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, shouldErr: `AggregateReconciler "Reconciler missing" must define Reconciler and/or DesiredResource`, }, { name: "DesiredResource", reconciler: &AggregateReconciler[*resources.TestResource]{ - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence[*resources.TestResource]{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, + Type: &resources.TestResource{}, + Request: req, + Reconciler: Sequence[*resources.TestResource]{}, DesiredResource: func(ctx context.Context, resource *resources.TestResource) (*resources.TestResource, error) { return nil, nil }, @@ -391,6 +386,19 @@ func TestChildReconciler_validate(t *testing.T) { { name: "valid", parent: &corev1.ConfigMap{}, + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, + ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, + }, + }, + { + name: "valid legacy", + parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -405,10 +413,12 @@ func TestChildReconciler_validate(t *testing.T) { reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "ChildType missing", // ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, }, { @@ -418,9 +428,11 @@ func TestChildReconciler_validate(t *testing.T) { Name: "ChildListType missing", ChildType: &corev1.Pod{}, // ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, }, { @@ -431,8 +443,10 @@ func TestChildReconciler_validate(t *testing.T) { ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, // DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, shouldErr: `ChildReconciler "DesiredChild missing" must implement DesiredChild`, }, @@ -444,8 +458,10 @@ func TestChildReconciler_validate(t *testing.T) { ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, // ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, shouldErr: `ChildReconciler "ReflectChildStatusOnParent missing" must implement ReflectChildStatusOnParent`, }, @@ -459,6 +475,8 @@ func TestChildReconciler_validate(t *testing.T) { DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, //MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + // normally ChildManger would be defined within init(), stub it here + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{}, }, shouldErr: `ChildReconciler "MergeBeforeUpdate missing" must implement MergeBeforeUpdate`, }, @@ -466,11 +484,13 @@ func TestChildReconciler_validate(t *testing.T) { name: "ListOptions", parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, }, }, @@ -478,12 +498,14 @@ func TestChildReconciler_validate(t *testing.T) { name: "ListOptions missing", parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "ListOptions missing", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + Name: "ListOptions missing", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, SkipOwnerReference: true, // ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, OurChild: func(resource *corev1.ConfigMap, child *corev1.Pod) bool { return true }, @@ -494,12 +516,14 @@ func TestChildReconciler_validate(t *testing.T) { name: "Finalizer without OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "Finalizer without OurChild", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + Name: "Finalizer without OurChild", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, Finalizer: "my-finalizer", }, shouldErr: `ChildReconciler "Finalizer without OurChild" must implement OurChild since owner references are not used`, @@ -508,12 +532,14 @@ func TestChildReconciler_validate(t *testing.T) { name: "SkipOwnerReference without OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "SkipOwnerReference without OurChild", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + Name: "SkipOwnerReference without OurChild", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, SkipOwnerReference: true, }, shouldErr: `ChildReconciler "SkipOwnerReference without OurChild" must implement OurChild since owner references are not used`, @@ -522,11 +548,13 @@ func TestChildReconciler_validate(t *testing.T) { name: "OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, OurChild: func(parent *corev1.ConfigMap, child *corev1.Pod) bool { return false }, }, }, @@ -537,7 +565,11 @@ func TestChildReconciler_validate(t *testing.T) { ctx := StashResourceType(context.TODO(), c.parent) err := c.reconciler.validate(ctx) if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { - t.Errorf("validate() error = %q, shouldErr %q", err.Error(), c.shouldErr) + var errString string + if err != nil { + errString = err.Error() + } + t.Errorf("validate() error = %q, shouldErr %q", errString, c.shouldErr) } }) } @@ -559,6 +591,20 @@ func TestChildSetReconciler_validate(t *testing.T) { { name: "valid", parent: &corev1.ConfigMap{}, + reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, + ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, + IdentifyChild: func(child *corev1.Pod) string { return "" }, + }, + }, + { + name: "valid legacy", + parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -574,10 +620,12 @@ func TestChildSetReconciler_validate(t *testing.T) { reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "ChildType missing", // ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, }, @@ -588,9 +636,11 @@ func TestChildSetReconciler_validate(t *testing.T) { Name: "ChildListType missing", ChildType: &corev1.Pod{}, // ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, }, @@ -602,8 +652,10 @@ func TestChildSetReconciler_validate(t *testing.T) { ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, // DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, shouldErr: `ChildSetReconciler "DesiredChildren missing" must implement DesiredChildren`, @@ -616,9 +668,11 @@ func TestChildSetReconciler_validate(t *testing.T) { ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, // ReflectChildrenStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - IdentifyChild: func(child *corev1.Pod) string { return "" }, + IdentifyChild: func(child *corev1.Pod) string { return "" }, }, shouldErr: `ChildSetReconciler "ReflectChildrenStatusOnParent missing" must implement ReflectChildrenStatusOnParent`, }, @@ -626,12 +680,14 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "IdentifyChild missing", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "IdentifyChild missing", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + Name: "IdentifyChild missing", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, // IdentifyChild: func(child *corev1.Pod) string { return "" }, }, shouldErr: `ChildSetReconciler "IdentifyChild missing" must implement IdentifyChild`, @@ -640,11 +696,13 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "ListOptions", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, @@ -653,12 +711,14 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "ListOptions missing", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "ListOptions missing", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + Name: "ListOptions missing", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, SkipOwnerReference: true, // ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, OurChild: func(resource *corev1.ConfigMap, child *corev1.Pod) bool { return true }, @@ -670,12 +730,14 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "Finalizer without OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "Finalizer without OurChild", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + Name: "Finalizer without OurChild", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, Finalizer: "my-finalizer", IdentifyChild: func(child *corev1.Pod) string { return "" }, }, @@ -685,12 +747,14 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "SkipOwnerReference without OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - Name: "SkipOwnerReference without OurChild", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + Name: "SkipOwnerReference without OurChild", + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, SkipOwnerReference: true, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, @@ -700,11 +764,13 @@ func TestChildSetReconciler_validate(t *testing.T) { name: "OurChild", parent: &corev1.ConfigMap{}, reconciler: &ChildSetReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildType: &corev1.Pod{}, + ChildListType: &corev1.PodList{}, + DesiredChildren: func(ctx context.Context, parent *corev1.ConfigMap) ([]*corev1.Pod, error) { return nil, nil }, + ChildObjectManager: &UpdatingObjectManager[*corev1.Pod]{ + MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, + }, ReflectChildrenStatusOnParent: func(ctx context.Context, parent *corev1.ConfigMap, result ChildSetResult[*corev1.Pod]) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, OurChild: func(parent *corev1.ConfigMap, child *corev1.Pod) bool { return false }, IdentifyChild: func(child *corev1.Pod) string { return "" }, }, @@ -884,28 +950,28 @@ func TestWithFinalizer_validate(t *testing.T) { } } -func TestResourceManager_validate(t *testing.T) { +func TestUpdatingObjectManager_validate(t *testing.T) { tests := []struct { name string - reconciler *ResourceManager[*resources.TestResource] + reconciler *UpdatingObjectManager[*resources.TestResource] shouldErr string expectedLogs []string }{ { name: "empty", - reconciler: &ResourceManager[*resources.TestResource]{}, - shouldErr: `ResourceManager "" must define MergeBeforeUpdate`, + reconciler: &UpdatingObjectManager[*resources.TestResource]{}, + shouldErr: `UpdatingObjectManager "" must define MergeBeforeUpdate`, }, { name: "valid", - reconciler: &ResourceManager[*resources.TestResource]{ + reconciler: &UpdatingObjectManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, }, { name: "Type missing", - reconciler: &ResourceManager[*resources.TestResource]{ + reconciler: &UpdatingObjectManager[*resources.TestResource]{ Name: "Type missing", // Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, @@ -913,16 +979,16 @@ func TestResourceManager_validate(t *testing.T) { }, { name: "MergeBeforeUpdate missing", - reconciler: &ResourceManager[*resources.TestResource]{ + reconciler: &UpdatingObjectManager[*resources.TestResource]{ Name: "MergeBeforeUpdate missing", Type: &resources.TestResource{}, // MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, - shouldErr: `ResourceManager "MergeBeforeUpdate missing" must define MergeBeforeUpdate`, + shouldErr: `UpdatingObjectManager "MergeBeforeUpdate missing" must define MergeBeforeUpdate`, }, { name: "HarmonizeImmutableFields", - reconciler: &ResourceManager[*resources.TestResource]{ + reconciler: &UpdatingObjectManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, HarmonizeImmutableFields: func(current, desired *resources.TestResource) {}, @@ -930,7 +996,7 @@ func TestResourceManager_validate(t *testing.T) { }, { name: "Sanitize", - reconciler: &ResourceManager[*resources.TestResource]{ + reconciler: &UpdatingObjectManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, Sanitize: func(child *resources.TestResource) interface{} { return child.Spec },