From eaa26f96c5a12e016c7104c5646eff524a5a0257 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 5 Apr 2023 09:40:05 -0400 Subject: [PATCH] Apply go 1.18 generics across reconciler-runtime (#349) This is a major breaking change that will affect all users of reconciler-runtime. There does not appear to be a way to transparently apply generics to an existing API that allows users to opt-in over time. User defined functions are now compile-time type checked, and IDEs will be able to offer completion for method signatures. At runtime these functions are directly invoked and are no longer called by reflection. Runtime type checking has been removed. Changes: - All existing deprecations are removed - All Reconcilers and SubReconcilers now operate on generic types of client.Object. - All reconciler Type/ChildType/ChildListType fields are now optional. They are only required if the generic type is an interface, or to define the apiVersion/kind for an Unstructured type. - SyncReconciler#Sync and #Finalize methods are split into two fields, one that is _WithResult and the default method which only returns an error. Signed-off-by: Scott Andrews --- README.md | 80 +- internal/resources/resource.go | 27 +- .../resources/resource_with_nilable_status.go | 27 +- internal/util.go | 30 + reconcilers/alias.go | 3 + reconcilers/enqueuer.go | 7 +- reconcilers/logger.go | 68 -- reconcilers/reconcilers.go | 992 +++++++----------- reconcilers/reconcilers_test.go | 881 ++++++++-------- reconcilers/reconcilers_validate_test.go | 856 +++------------ reconcilers/webhook.go | 51 +- reconcilers/webhook_test.go | 44 +- testing/aliases.go | 12 - testing/assert.go | 36 - testing/client.go | 10 - testing/config.go | 8 +- testing/reconciler.go | 18 +- testing/subreconciler.go | 53 +- validation/docs.go | 7 - validation/fielderrors.go | 156 --- validation/fielderrors_test.go | 231 ---- 21 files changed, 1165 insertions(+), 2432 deletions(-) create mode 100644 internal/util.go delete mode 100644 reconcilers/logger.go delete mode 100644 testing/assert.go delete mode 100644 validation/docs.go delete mode 100644 validation/fielderrors.go delete mode 100644 validation/fielderrors_test.go diff --git a/README.md b/README.md index aa83991..f1f1721 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,10 @@ The implementor is responsible for: Resource reconcilers tend to be quite simple, as they delegate their work to sub reconcilers. We'll use an example from projectriff of the Function resource, which uses Kpack to build images from a git repo. In this case the FunctionTargetImageReconciler resolves the target image for the function, and FunctionChildImageReconciler creates a child Kpack Image resource based on the resolve value. ```go -func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler { +func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*buildv1alpha1.Function] { return &reconcilers.ResourceReconciler{ Name: "Function", - Type: &buildv1alpha1.Function{}, - Reconciler: reconcilers.Sequence{ + Reconciler: reconcilers.Sequence[*buildv1alpha1.Function]{ FunctionTargetImageReconciler(c), FunctionChildImageReconciler(c), }, @@ -141,21 +140,19 @@ The resulting `ValidatingWebhookConfiguration` will have the current desired rul // AdmissionTriggerReconciler reconciles a ValidatingWebhookConfiguration object to // dynamically be notified of resource mutations. A less reliable, but potentially more // efficient than an informer watching each tracked resource. -func AdmissionTriggerReconciler(c reconcilers.Config) *reconcilers.AggregateReconciler { +func AdmissionTriggerReconciler(c reconcilers.Config) *reconcilers.AggregateReconciler[*admissionregistrationv1.ValidatingWebhookConfiguration] { return &reconcilers.AggregateReconciler{ Name: "AdmissionTrigger", - Type: &admissionregistrationv1.ValidatingWebhookConfiguration{}, - ListType: &admissionregistrationv1.ValidatingWebhookConfigurationList{}, - Request: reconcile.Request{ + Request: reconcilers.Request{ NamesspacedName: types.NamesspacedName{ // no namespace since ValidatingWebhookConfiguration is cluster scoped Name: "my-trigger", }, }, - Reconciler: reconcilers.Sequence{ + Reconciler: reconcilers.Sequence[*admissionregistrationv1.ValidatingWebhookConfiguration]{ DeriveWebhookRules(), }, - DesiredResource: func(ctx context.Context, resource *admissionregistrationv1.ValidatingWebhookConfiguration) (client.Object, error) { + DesiredResource: func(ctx context.Context, resource *admissionregistrationv1.ValidatingWebhookConfiguration) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) { // assumes other aspects of the webhook config are part of a preexisting // install, and that there is a server ready to receive the requests. rules := RetrieveWebhookRules(ctx) @@ -165,7 +162,7 @@ func AdmissionTriggerReconciler(c reconcilers.Config) *reconcilers.AggregateReco MergeBeforeUpdate: func(current, desired *admissionregistrationv1.ValidatingWebhookConfiguration) { current.Webhooks[0].Rules = desired.Webhooks[0].Rules }, - Sanitize: func(resource *admissionregistrationv1.ValidatingWebhookConfiguration) []admissionregistrationv1.RuleWithOperations { + Sanitize: func(resource *admissionregistrationv1.ValidatingWebhookConfiguration) interface{} { return resource.Webhooks[0].Rules }, @@ -215,8 +212,8 @@ When a resource is deleted that has pending finalizers, the Finalize method is c While sync reconcilers have the ability to do anything a reconciler can do, it's best to keep them focused on a single goal, letting the resource reconciler structure multiple sub reconcilers together. In this case, we use the reconciled resource and the client to resolve the target image and stash the value on the resource's status. The status is a good place to stash simple values that can be made public. More [advanced forms of stashing](#stash) are also available. Learn more about [status and its contract](#status). ```go -func FunctionTargetImageReconciler(c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ +func FunctionTargetImageReconciler(c reconcilers.Config) reconcilers.SubReconciler[*buildv1alpha1.Function] { + return &reconcilers.SyncReconciler[*buildv1alpha1.Function]{ Name: "TargetImage", Sync: func(ctx context.Context, resource *buildv1alpha1.Function) error { log := logr.FromContextOrDiscard(ctx) @@ -264,12 +261,9 @@ Using a finalizer means that the child resource will not use an owner reference. Now it's time to create the child Image resource that will do the work of building our Function. This reconciler looks more more complex than what we have seen so far, each function on the reconciler provides a focused hook into the lifecycle being orchestrated by the ChildReconciler. ```go -func FunctionChildImageReconciler(c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.ChildReconciler{ +func FunctionChildImageReconciler(c reconcilers.Config) reconcilers.SubReconciler[*buildv1alpha1.Function] { + return &reconcilers.ChildReconciler[*buildv1alpha1.Function, *kpackbuildv1alpha1.Image, *kpackbuildv1alpha1.ImageList]{ Name: "ChildImage", - ChildType: &kpackbuildv1alpha1.Image{}, - ChildListType: &kpackbuildv1alpha1.ImageList{}, - DesiredChild: func(ctx context.Context, parent *buildv1alpha1.Function) (*kpackbuildv1alpha1.Image, error) { if parent.Spec.Source == nil { // don't create an Image, and delete any existing Image @@ -363,20 +357,20 @@ Higher order reconcilers are SubReconcilers that do not perform work directly, b A [`CastResource`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#CastResource) (formerly CastParent) casts the ResourceReconciler's type by projecting the resource data onto a new struct. Casting the reconciled resource is useful to create cross cutting reconcilers that can operate on common portion of multiple resource kinds, commonly referred to as a duck type. +The `CastResource` can also be used to bridge a `SubReconciler` for a specific struct to a generic SubReconciler that can process any object by using `client.Object` as the CastType generic type. In this case, the resource is passed through without coercion. + **Example:** ```go -func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler { - return &reconcilers.ResourceReconciler{ +func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*buildv1alpha1.Function] { + return &reconcilers.ResourceReconciler[*buildv1alpha1.Function]{ Name: "Function", - Type: &buildv1alpha1.Function{}, - Reconciler: reconcilers.Sequence{ - &reconcilers.CastResource{ - Type: &duckv1alpha1.ImageRef{}, + Reconciler: reconcilers.Sequence[*buildv1alpha1.Function]{ + &reconcilers.CastResource[*buildv1alpha1.Function, *duckv1alpha1.ImageRef]{ // Reconciler that now operates on the ImageRef type. This SubReconciler is likely // shared between multiple ResourceReconcilers that operate on different types, // otherwise it would be easier to work directly with the Function type directly. - Reconciler: &reconcilers.SyncReconciler{ + Reconciler: &reconcilers.SyncReconciler[*duckv1alpha1.ImageRef]{ Sync: func(ctx context.Context, resource *duckv1alpha1.ImageRef) error { // do something with the duckv1alpha1.ImageRef instead of a buildv1alpha1.Function return nil @@ -400,11 +394,10 @@ A [`Sequence`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reco A Sequence is commonly used in a ResourceReconciler, but may be used anywhere a SubReconciler is accepted. ```go -func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler { - return &reconcilers.ResourceReconciler{ +func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*buildv1alpha1.Function] { + return &reconcilers.ResourceReconciler[*buildv1alpha1.Function]{ Name: "Function", - Type: &buildv1alpha1.Function{}, - Reconciler: reconcilers.Sequence{ + Reconciler: reconcilers.Sequence[*buildv1alpha1.Function]{ FunctionTargetImageReconciler(c), FunctionChildImageReconciler(c), }, @@ -424,9 +417,9 @@ func FunctionReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler { `WithConfig` can be used to change the REST Config backing the clients. This could be to make requests to the same cluster with a user defined service account, or target an entirely different Kubernetes cluster. ```go -func SwapRESTConfig(rc *rest.Config) *reconcilers.SubReconciler { - return &reconcilers.WithConfig{ - Reconciler: reconcilers.Sequence{ +func SwapRESTConfig(rc *rest.Config) *reconcilers.SubReconciler[*resources.MyResource] { + return &reconcilers.WithConfig[*resources.MyResource]{ + Reconciler: reconcilers.Sequence[*resources.MyResource]{ LookupReferenceDataReconciler(), DoSomethingChildReconciler(), }, @@ -456,15 +449,15 @@ The [Finalizers](#finalizers) utilities are used to manage the finalizer on the `WithFinalizer` can be used to wrap any other [SubReconciler](#subreconciler), which can then safely allocate external state while the resource is not terminating, and then cleanup that state once the resource is terminating. ```go -func SyncExternalState() *reconcilers.SubReconciler { - return &reconcilers.WithFinalizer{ +func SyncExternalState() *reconcilers.SubReconciler[*resources.MyResource] { + return &reconcilers.WithFinalizer[*resources.MyResource]{ Finalizer: "unique.finalizer.name" - Reconciler: &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) error { + Reconciler: &reconcilers.SyncReconciler[*resources.MyResource]{ + Sync: func(ctx context.Context, resource *resources.MyResource) error { // allocate external state return nil }, - Finalize: func(ctx context.Context, resource *resources.TestResource) error { + Finalize: func(ctx context.Context, resource *resources.MyResource) error { // cleanup the external state return nil }, @@ -490,11 +483,10 @@ Testing can be done on the reconciler directly with [SubReconcilerTests](#subrec The Service Binding controller uses a mutating webhook to intercept the creation and updating of workload resources. It projects services into the workload based on ServiceBindings that reference that workload, mutating the resource. If the resource is mutated, a patch is automatically created and added to the webhook response. The webhook allows workloads to be bound at admission time. ```go -func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebhookAdapter { +func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured] { return &reconcilers.AdmissionWebhookAdapter{ Name: "AdmissionProjectorWebhook", - Type: &unstructured.Unstructured{}, - Reconciler: &reconcilers.SyncReconciler{ + Reconciler: &reconcilers.SyncReconciler[*unstructured.Unstructured]{ Sync: func(ctx context.Context, workload *unstructured.Unstructured) error { c := reconcilers.RetrieveConfigOrDie(ctx) @@ -620,7 +612,7 @@ Like with the tracking example, the processor reconciler in projectriff also loo processor := ... processorImagesConfigMap := ... -rts := rtesting.SubReconcilerTests{ +rts := rtesting.SubReconcilerTests[*streamingv1alpha1.Processor]{ "missing images configmap": { Resource: processor, ExpectTracks: []rtesting.TrackRequest{ @@ -642,7 +634,7 @@ rts := rtesting.SubReconcilerTests{ }, } -rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { +rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*streamingv1alpha1.Processor], c reconcilers.Config) reconcilers.SubReconciler[*streamingv1alpha1.Processor] { return streaming.ProcessorSyncProcessorImages(c, testSystemNamespace) }) ``` @@ -788,8 +780,8 @@ The tracked resource and its tracker are managed by reference and do not need co The stream gateways in projectriff fetch the image references they use to run from a ConfigMap. When the ConfigMap changes, we want to detect and rollout the updated images. ```go -func InMemoryGatewaySyncConfigReconciler(c reconcilers.Config, namespace string) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ +func InMemoryGatewaySyncConfigReconciler(c reconcilers.Config, namespace string) reconcilers.SubReconciler[*streamingv1alpha1.InMemoryGateway] { + return &reconcilers.SyncReconciler[*streamingv1alpha1.InMemoryGateway]{ Name: "SyncConfig", Sync: func(ctx context.Context, resource *streamingv1alpha1.InMemoryGateway) error { log := logr.FromContextOrDiscard(ctx) @@ -874,7 +866,7 @@ A minimal test case for a sub reconciler that adds a finalizer may look like: ... { Name: "add 'test.finalizer' finalizer", - Resource: resourceDie, + Resource: resourceDie.DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resourceDie, scheme, corev1.EventTypeNormal, "FinalizerPatched", `Patched finalizer %q`, "test.finalizer"), diff --git a/internal/resources/resource.go b/internal/resources/resource.go index 08b49c5..7654885 100644 --- a/internal/resources/resource.go +++ b/internal/resources/resource.go @@ -10,19 +10,20 @@ import ( "fmt" "github.com/vmware-labs/reconciler-runtime/apis" - "github.com/vmware-labs/reconciler-runtime/validation" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/scheme" "sigs.k8s.io/controller-runtime/pkg/webhook" ) var ( - _ webhook.Defaulter = &TestResource{} - _ client.Object = &TestResource{} - _ validation.FieldValidator = &TestResource{} + _ webhook.Defaulter = &TestResource{} + _ webhook.Validator = &TestResource{} + _ client.Object = &TestResource{} ) // +kubebuilder:object:root=true @@ -43,12 +44,24 @@ func (r *TestResource) Default() { r.Spec.Fields["Defaulter"] = "ran" } -func (r *TestResource) Validate() validation.FieldErrors { - errs := validation.FieldErrors{} +func (r *TestResource) ValidateCreate() error { + return r.validate().ToAggregate() +} + +func (r *TestResource) ValidateUpdate(old runtime.Object) error { + return r.validate().ToAggregate() +} + +func (r *TestResource) ValidateDelete() error { + return nil +} + +func (r *TestResource) validate() field.ErrorList { + errs := field.ErrorList{} if r.Spec.Fields != nil { if _, ok := r.Spec.Fields["invalid"]; ok { - errs = errs.Also(validation.ErrInvalidValue(r.Spec.Fields["invalid"], "spec.fields.invalid")) + field.Invalid(field.NewPath("spec", "fields", "invalid"), r.Spec.Fields["invalid"], "") } } diff --git a/internal/resources/resource_with_nilable_status.go b/internal/resources/resource_with_nilable_status.go index 3daa584..a0c8572 100644 --- a/internal/resources/resource_with_nilable_status.go +++ b/internal/resources/resource_with_nilable_status.go @@ -6,16 +6,17 @@ SPDX-License-Identifier: Apache-2.0 package resources import ( - "github.com/vmware-labs/reconciler-runtime/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook" ) var ( - _ webhook.Defaulter = &TestResourceNilableStatus{} - _ client.Object = &TestResourceNilableStatus{} - _ validation.FieldValidator = &TestResourceNilableStatus{} + _ webhook.Defaulter = &TestResourceNilableStatus{} + _ webhook.Validator = &TestResourceNilableStatus{} + _ client.Object = &TestResourceNilableStatus{} ) // +kubebuilder:object:root=true @@ -36,12 +37,24 @@ func (r *TestResourceNilableStatus) Default() { r.Spec.Fields["Defaulter"] = "ran" } -func (r *TestResourceNilableStatus) Validate() validation.FieldErrors { - errs := validation.FieldErrors{} +func (r *TestResourceNilableStatus) ValidateCreate() error { + return r.validate().ToAggregate() +} + +func (r *TestResourceNilableStatus) ValidateUpdate(old runtime.Object) error { + return r.validate().ToAggregate() +} + +func (r *TestResourceNilableStatus) ValidateDelete() error { + return nil +} + +func (r *TestResourceNilableStatus) validate() field.ErrorList { + errs := field.ErrorList{} if r.Spec.Fields != nil { if _, ok := r.Spec.Fields["invalid"]; ok { - errs = errs.Also(validation.ErrInvalidValue(r.Spec.Fields["invalid"], "spec.fields.invalid")) + field.Invalid(field.NewPath("spec", "fields", "invalid"), r.Spec.Fields["invalid"], "") } } diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..6629b50 --- /dev/null +++ b/internal/util.go @@ -0,0 +1,30 @@ +/* +Copyright 2023 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package internal + +import "reflect" + +// IsNil returns true if the value is nilable and nil +func IsNil(val interface{}) bool { + // return val == nil + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.Chan: + return v.IsNil() + case reflect.Func: + return v.IsNil() + case reflect.Interface: + return v.IsNil() + case reflect.Map: + return v.IsNil() + case reflect.Ptr: + return v.IsNil() + case reflect.Slice: + return v.IsNil() + default: + return false + } +} diff --git a/reconcilers/alias.go b/reconcilers/alias.go index 905fa22..cab7d6c 100644 --- a/reconcilers/alias.go +++ b/reconcilers/alias.go @@ -8,9 +8,12 @@ package reconcilers import ( "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) type Builder = builder.Builder type Manager = manager.Manager type Kind = source.Kind +type Request = reconcile.Request +type Result = reconcile.Result diff --git a/reconcilers/enqueuer.go b/reconcilers/enqueuer.go index 24971ab..7ed3033 100644 --- a/reconcilers/enqueuer.go +++ b/reconcilers/enqueuer.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/vmware-labs/reconciler-runtime/tracker" ) @@ -19,8 +18,8 @@ import ( func EnqueueTracked(ctx context.Context, by client.Object) handler.EventHandler { c := RetrieveConfigOrDie(ctx) return handler.EnqueueRequestsFromMapFunc( - func(a client.Object) []reconcile.Request { - var requests []reconcile.Request + func(a client.Object) []Request { + var requests []Request gvks, _, err := c.Scheme().ObjectKinds(by) if err != nil { @@ -32,7 +31,7 @@ func EnqueueTracked(ctx context.Context, by client.Object) handler.EventHandler types.NamespacedName{Namespace: a.GetNamespace(), Name: a.GetName()}, ) for _, item := range c.Tracker.Lookup(ctx, key) { - requests = append(requests, reconcile.Request{NamespacedName: item}) + requests = append(requests, Request{NamespacedName: item}) } return requests diff --git a/reconcilers/logger.go b/reconcilers/logger.go deleted file mode 100644 index 5e6e35b..0000000 --- a/reconcilers/logger.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2022 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -package reconcilers - -import ( - "fmt" - "sync" - - "github.com/go-logr/logr" -) - -var ( - _ logr.LogSink = (*warnOnceLogSink)(nil) -) - -// Deprecated -func newWarnOnceLogger(log logr.Logger) logr.Logger { - return logr.New(&warnOnceLogSink{ - sink: log.GetSink(), - }) -} - -// Deprecated -type warnOnceLogSink struct { - sink logr.LogSink - once sync.Once -} - -func (s *warnOnceLogSink) Init(info logr.RuntimeInfo) { - s.sink.Init(info) -} - -func (s *warnOnceLogSink) Enabled(level int) bool { - return s.sink.Enabled(level) -} - -func (s *warnOnceLogSink) Info(level int, msg string, keysAndValues ...interface{}) { - s.warn() - s.sink.Info(level, msg, keysAndValues...) -} - -func (s *warnOnceLogSink) Error(err error, msg string, keysAndValues ...interface{}) { - s.warn() - s.sink.Error(err, msg, keysAndValues...) -} - -func (s *warnOnceLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { - return &warnOnceLogSink{ - sink: s.sink.WithValues(keysAndValues...), - once: s.once, - } -} - -func (s *warnOnceLogSink) WithName(name string) logr.LogSink { - return &warnOnceLogSink{ - sink: s.sink.WithName(name), - once: s.once, - } -} - -func (s *warnOnceLogSink) warn() { - s.once.Do(func() { - s.sink.Error(fmt.Errorf("Config.Log is deprecated"), "use a logger from the context: `log := logr.FromContext(ctx)`") - }) -} diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 9e84fb7..8f0b9af 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -36,12 +36,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/controller-runtime/pkg/webhook" + "github.com/vmware-labs/reconciler-runtime/internal" "github.com/vmware-labs/reconciler-runtime/tracker" ) var ( - _ reconcile.Reconciler = (*ResourceReconciler)(nil) - _ reconcile.Reconciler = (*AggregateReconciler)(nil) + _ reconcile.Reconciler = (*ResourceReconciler[client.Object])(nil) + _ reconcile.Reconciler = (*AggregateReconciler[client.Object])(nil) ) // Config holds common resources for controllers. The configuration may be @@ -51,11 +52,6 @@ type Config struct { APIReader client.Reader Recorder record.EventRecorder Tracker tracker.Tracker - - // Deprecated: use a logger from the context instead. For example: - // * `log, err := logr.FromContext(ctx)` - // * `log := logr.FromContextOrDiscard(ctx)` - Log logr.Logger } func (c Config) IsEmpty() bool { @@ -68,7 +64,6 @@ func (c Config) WithCluster(cluster cluster.Cluster) Config { Client: cluster.GetClient(), APIReader: cluster.GetAPIReader(), Recorder: cluster.GetEventRecorderFor("controller"), - Log: c.Log, Tracker: c.Tracker, } } @@ -89,23 +84,17 @@ func (c Config) TrackAndGet(ctx context.Context, key types.NamespacedName, obj c // NewConfig creates a Config for a specific API type. Typically passed into a // reconciler. func NewConfig(mgr ctrl.Manager, apiType client.Object, syncPeriod time.Duration) Config { - name := typeName(apiType) - log := newWarnOnceLogger(ctrl.Log.WithName("controllers").WithName(name)) return Config{ - Log: log, Tracker: tracker.New(2 * syncPeriod), }.WithCluster(mgr) } -// Deprecated use ResourceReconciler -type ParentReconciler = ResourceReconciler - // ResourceReconciler is a controller-runtime reconciler that reconciles a given // existing resource. The Type resource is fetched for the reconciler // request and passed in turn to each SubReconciler. Finally, the reconciled // resource's status is compared with the original status, updating the API // server if needed. -type ResourceReconciler struct { +type ResourceReconciler[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `{Type}ResourceReconciler`. Ideally // unique, but not required to be so. // @@ -118,28 +107,43 @@ type ResourceReconciler struct { // +optional Setup func(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error - // Type of resource to reconcile - Type client.Object + // Type of resource to reconcile. Required when the generic type is not a + // struct, or is unstructured. + // + // +optional + Type Type // Reconciler is called for each reconciler request with the resource being reconciled. // Typically, Reconciler is a Sequence of multiple SubReconcilers. // // When HaltSubReconcilers is returned as an error, execution continues as if no error was // returned. - Reconciler SubReconciler + Reconciler SubReconciler[Type] Config Config + + lazyInit sync.Once } -func (r *ResourceReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { +func (r *ResourceReconciler[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("%sResourceReconciler", typeName(r.Type)) + } + }) +} + +func (r *ResourceReconciler[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { _, err := r.SetupWithManagerYieldingController(ctx, mgr) return err } -func (r *ResourceReconciler) SetupWithManagerYieldingController(ctx context.Context, mgr ctrl.Manager) (controller.Controller, error) { - if r.Name == "" { - r.Name = fmt.Sprintf("%sResourceReconciler", typeName(r.Type)) - } +func (r *ResourceReconciler[T]) SetupWithManagerYieldingController(ctx context.Context, mgr ctrl.Manager) (controller.Controller, error) { + r.init() log := logr.FromContextOrDiscard(ctx). WithName(r.Name). @@ -167,12 +171,7 @@ func (r *ResourceReconciler) SetupWithManagerYieldingController(ctx context.Cont return bldr.Build(r) } -func (r *ResourceReconciler) validate(ctx context.Context) error { - // validate Type value - if r.Type == nil { - return fmt.Errorf("ResourceReconciler %q must define Type", r.Name) - } - +func (r *ResourceReconciler[T]) validate(ctx context.Context) error { // validate Reconciler value if r.Reconciler == nil { return fmt.Errorf("ResourceReconciler %q must define Reconciler", r.Name) @@ -213,7 +212,9 @@ func (r *ResourceReconciler) validate(ctx context.Context) error { return nil } -func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *ResourceReconciler[T]) Reconcile(ctx context.Context, req Request) (Result, error) { + r.init() + ctx = WithStash(ctx) c := r.Config @@ -228,21 +229,21 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c ctx = StashOriginalConfig(ctx, c) ctx = StashOriginalResourceType(ctx, r.Type) ctx = StashResourceType(ctx, r.Type) - originalResource := r.Type.DeepCopyObject().(client.Object) + originalResource := r.Type.DeepCopyObject().(T) if err := c.Get(ctx, req.NamespacedName, originalResource); err != nil { if apierrs.IsNotFound(err) { // we'll ignore not-found errors, since they can't be fixed by an immediate // requeue (we'll need to wait for a new notification), and we can get them // on deleted requests. - return ctrl.Result{}, nil + return Result{}, nil } log.Error(err, "unable to fetch resource") - return ctrl.Result{}, err + return Result{}, err } - resource := originalResource.DeepCopyObject().(client.Object) + resource := originalResource.DeepCopyObject().(T) - if defaulter, ok := resource.(webhook.Defaulter); ok { + if defaulter, ok := client.Object(resource).(webhook.Defaulter); ok { // resource.Default() defaulter.Default() } @@ -262,7 +263,7 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c log.Error(updateErr, "unable to update status") c.Recorder.Eventf(resource, corev1.EventTypeWarning, "StatusUpdateFailed", "Failed to update status: %v", updateErr) - return ctrl.Result{}, updateErr + return Result{}, updateErr } c.Recorder.Eventf(resource, corev1.EventTypeNormal, "StatusUpdated", "Updated status") @@ -272,22 +273,22 @@ func (r *ResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return result, err } -func (r *ResourceReconciler) reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *ResourceReconciler[T]) reconcile(ctx context.Context, resource T) (Result, error) { if resource.GetDeletionTimestamp() != nil && len(resource.GetFinalizers()) == 0 { // resource is being deleted and has no pending finalizers, nothing to do - return ctrl.Result{}, nil + return Result{}, nil } result, err := r.Reconciler.Reconcile(ctx, resource) if err != nil && !errors.Is(err, HaltSubReconcilers) { - return ctrl.Result{}, err + return Result{}, err } r.copyGeneration(resource) return result, nil } -func (r *ResourceReconciler) initializeConditions(obj client.Object) { +func (r *ResourceReconciler[T]) initializeConditions(obj T) { status := r.status(obj) if status == nil { return @@ -302,7 +303,7 @@ func (r *ResourceReconciler) initializeConditions(obj client.Object) { initializeConditions.Call([]reflect.Value{}) } -func (r *ResourceReconciler) conditions(obj client.Object) []metav1.Condition { +func (r *ResourceReconciler[T]) conditions(obj T) []metav1.Condition { // return obj.Status.Conditions status := r.status(obj) if status == nil { @@ -324,7 +325,7 @@ func (r *ResourceReconciler) conditions(obj client.Object) []metav1.Condition { return conditions } -func (r *ResourceReconciler) copyGeneration(obj client.Object) { +func (r *ResourceReconciler[T]) copyGeneration(obj T) { // obj.Status.ObservedGeneration = obj.Generation status := r.status(obj) if status == nil { @@ -346,16 +347,16 @@ func (r *ResourceReconciler) copyGeneration(obj client.Object) { observedGenerationValue.SetInt(generation) } -func (r *ResourceReconciler) hasStatus(obj client.Object) bool { +func (r *ResourceReconciler[T]) hasStatus(obj T) bool { status := r.status(obj) return status != nil } -func (r *ResourceReconciler) status(obj client.Object) interface{} { - if obj == nil { +func (r *ResourceReconciler[T]) status(obj T) interface{} { + if client.Object(obj) == nil { return nil } - if u, ok := obj.(*unstructured.Unstructured); ok { + if u, ok := client.Object(obj).(*unstructured.Unstructured); ok { return u.UnstructuredContent()["status"] } statusValue := reflect.ValueOf(obj).Elem().FieldByName("Status") @@ -369,10 +370,10 @@ func (r *ResourceReconciler) status(obj client.Object) interface{} { } // syncLastTransitionTime restores a condition's LastTransitionTime value for -// each proposed condition that is otherwise equivlent to the original value. +// each proposed condition that is otherwise equivalent to the original value. // This method is useful to prevent updating the status for a resource that is // otherwise unchanged. -func (r *ResourceReconciler) syncLastTransitionTime(proposed, original []metav1.Condition) { +func (r *ResourceReconciler[T]) syncLastTransitionTime(proposed, original []metav1.Condition) { for _, o := range original { for i := range proposed { p := &proposed[i] @@ -394,7 +395,7 @@ func (r *ResourceReconciler) syncLastTransitionTime(proposed, original []metav1. // request and passed in turn to each SubReconciler. Finally, the reconciled // resource's status is compared with the original status, updating the API // server if needed. -type AggregateReconciler struct { +type AggregateReconciler[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `{Type}ResourceReconciler`. Ideally // unique, but not required to be so. // @@ -405,13 +406,17 @@ type AggregateReconciler struct { // will run with. It's common to setup field indexes and watch resources. // // +optional - Setup func(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error + Setup func(ctx context.Context, mgr Manager, bldr *Builder) error + + // Type of resource to reconcile. Required when the generic type is not a + // struct, or is unstructured. + // + // +optional + Type Type - // Type of resource to reconcile - Type client.Object // Request of resource to reconcile. Only the specific resource matching the namespace and name // is reconciled. The namespace may be empty for cluster scoped resources. - Request ctrl.Request + Request Request // Reconciler is called for each reconciler request with the resource being reconciled. // Typically, Reconciler is a Sequence of multiple SubReconcilers. @@ -420,68 +425,59 @@ type AggregateReconciler struct { // returned. // // +optional - Reconciler SubReconciler + Reconciler SubReconciler[Type] // DesiredResource returns the desired resource to create/update, or nil if // the resource should not exist. // - // Expected function signature: - // func(ctx context.Context, resource client.Object) (client.Object, error) - // // +optional - DesiredResource interface{} + DesiredResource func(ctx context.Context, resource Type) (Type, error) // 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. // - // Expected function signature: - // func(current, desired client.Object) - // // +optional - HarmonizeImmutableFields interface{} + 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. - // - // Expected function signature: - // func(current, desired client.Object) - MergeBeforeUpdate interface{} - - // Deprecated SemanticEquals is no longer used, the field can be removed. Equality is - // now determined based on the resource mutated by MergeBeforeUpdate - SemanticEquals interface{} + 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. // - // Expected function signature: - // func(resource client.Object) interface{} - // // +optional - Sanitize interface{} + Sanitize func(resource Type) interface{} Config Config // stamp manages the lifecycle of the aggregated resource. - stamp *ResourceManager + stamp *ResourceManager[Type] lazyInit sync.Once } -func (r *AggregateReconciler) init() { +func (r *AggregateReconciler[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("%sAggregateReconciler", typeName(r.Type)) + } if r.Reconciler == nil { - r.Reconciler = Sequence{} + r.Reconciler = Sequence[T]{} } if r.DesiredResource == nil { - r.DesiredResource = func(ctx context.Context, resource client.Object) (client.Object, error) { + r.DesiredResource = func(ctx context.Context, resource T) (T, error) { return resource, nil } } - r.stamp = &ResourceManager{ + r.stamp = &ResourceManager[T]{ Name: r.Name, Type: r.Type, @@ -492,15 +488,13 @@ func (r *AggregateReconciler) init() { }) } -func (r *AggregateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { +func (r *AggregateReconciler[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { _, err := r.SetupWithManagerYieldingController(ctx, mgr) return err } -func (r *AggregateReconciler) SetupWithManagerYieldingController(ctx context.Context, mgr ctrl.Manager) (controller.Controller, error) { - if r.Name == "" { - r.Name = fmt.Sprintf("%sAggregateReconciler", typeName(r.Type)) - } +func (r *AggregateReconciler[T]) SetupWithManagerYieldingController(ctx context.Context, mgr ctrl.Manager) (controller.Controller, error) { + r.init() log := logr.FromContextOrDiscard(ctx). WithName(r.Name). @@ -519,8 +513,6 @@ func (r *AggregateReconciler) SetupWithManagerYieldingController(ctx context.Con return nil, err } - r.init() - bldr := ctrl.NewControllerManagedBy(mgr).For(r.Type) if r.Setup != nil { if err := r.Setup(ctx, mgr, bldr); err != nil { @@ -536,11 +528,7 @@ func (r *AggregateReconciler) SetupWithManagerYieldingController(ctx context.Con return bldr.Build(r) } -func (r *AggregateReconciler) validate(ctx context.Context) error { - // validate Type value - if r.Type == nil { - return fmt.Errorf("AggregateReconciler %q must define Type", r.Name) - } +func (r *AggregateReconciler[T]) validate(ctx context.Context) error { // validate Request value if r.Request.Name == "" { return fmt.Errorf("AggregateReconciler %q must define Request", r.Name) @@ -551,27 +539,15 @@ func (r *AggregateReconciler) validate(ctx context.Context) error { return fmt.Errorf("AggregateReconciler %q must define Reconciler and/or DesiredResource", r.Name) } - // validate DesiredResource function signature: - // nil - // func(ctx context.Context, resource client.Object) (client.Object, error) - if r.DesiredResource != nil { - fn := reflect.TypeOf(r.DesiredResource) - if fn.NumIn() != 2 || fn.NumOut() != 2 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(1)) || - !reflect.TypeOf(r.Type).AssignableTo(fn.Out(0)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(1)) { - return fmt.Errorf("AggregateReconciler %q must implement DesiredResource: nil | func(context.Context, %s) (%s, error), found: %s", r.Name, reflect.TypeOf(r.Type), reflect.TypeOf(r.Type), fn) - } - } - return nil } -func (r *AggregateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *AggregateReconciler[T]) Reconcile(ctx context.Context, req Request) (Result, error) { + r.init() + if req.Namespace != r.Request.Namespace || req.Name != r.Request.Name { // ignore other requests - return ctrl.Result{}, nil + return Result{}, nil } ctx = WithStash(ctx) @@ -589,9 +565,7 @@ func (r *AggregateReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( ctx = StashOriginalResourceType(ctx, r.Type) ctx = StashResourceType(ctx, r.Type) - r.init() - - resource := r.Type.DeepCopyObject().(client.Object) + resource := r.Type.DeepCopyObject().(T) if err := c.Get(ctx, req.NamespacedName, resource); err != nil { if apierrs.IsNotFound(err) { // not found is ok @@ -599,13 +573,13 @@ func (r *AggregateReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( resource.SetName(r.Request.Name) } else { log.Error(err, "unable to fetch resource") - return ctrl.Result{}, err + return Result{}, err } } if resource.GetDeletionTimestamp() != nil { // resource is being deleted, nothing to do - return ctrl.Result{}, nil + return Result{}, nil } result, err := r.Reconciler.Reconcile(ctx, resource) @@ -622,19 +596,21 @@ func (r *AggregateReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( }) desired, err := r.desiredResource(ctx, resource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } _, err = r.stamp.Manage(ctx, resource, resource, desired) if err != nil { - return ctrl.Result{}, err + return Result{}, err } return result, nil } -func (r *AggregateReconciler) desiredResource(ctx context.Context, resource client.Object) (client.Object, error) { +func (r *AggregateReconciler[T]) desiredResource(ctx context.Context, resource T) (T, error) { + var nilT T + if resource.GetDeletionTimestamp() != nil { // the reconciled resource is pending deletion, cleanup the child resource - return nil, nil + return nilT, nil } fn := reflect.ValueOf(r.DesiredResource) @@ -642,9 +618,9 @@ func (r *AggregateReconciler) desiredResource(ctx context.Context, resource clie reflect.ValueOf(ctx), reflect.ValueOf(resource.DeepCopyObject()), }) - var obj client.Object + var obj T if !out[0].IsNil() { - obj = out[0].Interface().(client.Object) + obj = out[0].Interface().(T) } var err error if !out[1].IsNil() { @@ -660,17 +636,17 @@ const resourceTypeStashKey StashKey = "reconciler-runtime:resourceType" const originalResourceTypeStashKey StashKey = "reconciler-runtime:originalResourceType" const additionalConfigsStashKey StashKey = "reconciler-runtime:additionalConfigs" -func StashRequest(ctx context.Context, req ctrl.Request) context.Context { +func StashRequest(ctx context.Context, req Request) context.Context { return context.WithValue(ctx, requestStashKey, req) } // RetrieveRequest returns the reconciler Request from the context, or empty if not found. -func RetrieveRequest(ctx context.Context) ctrl.Request { +func RetrieveRequest(ctx context.Context) Request { value := ctx.Value(requestStashKey) - if req, ok := value.(ctrl.Request); ok { + if req, ok := value.(Request); ok { return req } - return ctrl.Request{} + return Request{} } func StashConfig(ctx context.Context, config Config) context.Context { @@ -695,16 +671,10 @@ func RetrieveConfigOrDie(ctx context.Context) Config { return config } -// Deprecated use StashOriginalConfig -var StashParentConfig = StashOriginalConfig - func StashOriginalConfig(ctx context.Context, resourceConfig Config) context.Context { return context.WithValue(ctx, originalConfigStashKey, resourceConfig) } -// Deprecated use RetrieveOriginalConfig -var RetrieveParentConfig = RetrieveOriginalConfig - // RetrieveOriginalConfig returns the Config from the context used to load the reconciled resource. An // error is returned if not found. func RetrieveOriginalConfig(ctx context.Context) (Config, error) { @@ -715,9 +685,6 @@ func RetrieveOriginalConfig(ctx context.Context) (Config, error) { return Config{}, fmt.Errorf("resource config must exist on the context. Check that the context is from a ResourceReconciler") } -// Deprecated use RetrieveOriginalConfigOrDie -var RetrieveParentConfigOrDie = RetrieveOriginalConfigOrDie - // RetrieveOriginalConfigOrDie returns the Config from the context used to load the reconciled resource. // Panics if not found. func RetrieveOriginalConfigOrDie(ctx context.Context) Config { @@ -728,16 +695,10 @@ func RetrieveOriginalConfigOrDie(ctx context.Context) Config { return config } -// Deprecated use StashResourceType -var StashCastParentType = StashResourceType - func StashResourceType(ctx context.Context, currentType client.Object) context.Context { return context.WithValue(ctx, resourceTypeStashKey, currentType) } -// Deprecated use RetrieveResourceType -var RetrieveCastParentType = RetrieveResourceType - // RetrieveResourceType returns the reconciled resource type object, or nil if not found. func RetrieveResourceType(ctx context.Context) client.Object { value := ctx.Value(resourceTypeStashKey) @@ -747,16 +708,10 @@ func RetrieveResourceType(ctx context.Context) client.Object { return nil } -// Deprecated use StashOriginalResourceType -var StashParentType = StashOriginalResourceType - func StashOriginalResourceType(ctx context.Context, resourceType client.Object) context.Context { return context.WithValue(ctx, originalResourceTypeStashKey, resourceType) } -// Deprecated use RetrieveOriginalResourceType -var RetrieveParentType = RetrieveOriginalResourceType - // RetrieveOriginalResourceType returns the reconciled resource type object, or nil if not found. func RetrieveOriginalResourceType(ctx context.Context) client.Object { value := ctx.Value(originalResourceTypeStashKey) @@ -782,18 +737,18 @@ func RetrieveAdditionalConfigs(ctx context.Context) map[string]Config { // SubReconciler are participants in a larger reconciler request. The resource // being reconciled is passed directly to the sub reconciler. -type SubReconciler interface { +type SubReconciler[Type client.Object] interface { SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error - Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) + Reconcile(ctx context.Context, resource Type) (Result, error) } var ( - _ SubReconciler = (*SyncReconciler)(nil) - _ SubReconciler = (*ChildReconciler)(nil) - _ SubReconciler = (Sequence)(nil) - _ SubReconciler = (*CastResource)(nil) - _ SubReconciler = (*WithConfig)(nil) - _ SubReconciler = (*WithFinalizer)(nil) + _ SubReconciler[client.Object] = (*SyncReconciler[client.Object])(nil) + _ SubReconciler[client.Object] = (*ChildReconciler[client.Object, client.Object, client.ObjectList])(nil) + _ SubReconciler[client.Object] = (Sequence[client.Object])(nil) + _ SubReconciler[client.Object] = (*CastResource[client.Object, client.Object])(nil) + _ SubReconciler[client.Object] = (*WithConfig[client.Object])(nil) + _ SubReconciler[client.Object] = (*WithFinalizer[client.Object])(nil) ) var ( @@ -807,7 +762,7 @@ var ( // SyncReconciler is a sub reconciler for custom reconciliation logic. No // behavior is defined directly. -type SyncReconciler struct { +type SyncReconciler[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `SyncReconciler`. Ideally unique, but // not required to be so. // @@ -828,24 +783,37 @@ type SyncReconciler struct { // If SyncDuringFinalization is true this method is called when the resource is pending // deletion. This is useful if the reconciler is managing reference data. // - // Expected function signature: - // func(ctx context.Context, resource client.Object) error - // func(ctx context.Context, resource client.Object) (ctrl.Result, error) - Sync interface{} + // Mutually exclusive with SyncWithResult + Sync func(ctx context.Context, resource Type) error + + // SyncWithResult does whatever work is necessary for the reconciler. + // + // If SyncDuringFinalization is true this method is called when the resource is pending + // deletion. This is useful if the reconciler is managing reference data. + // + // Mutually exclusive with Sync + SyncWithResult func(ctx context.Context, resource Type) (Result, error) + + // Finalize does whatever work is necessary for the reconciler when the resource is pending + // deletion. If this reconciler sets a finalizer it should do the necessary work to clean up + // state the finalizer represents and then clear the finalizer. + // + // Mutually exclusive with FinalizeWithResult + // + // +optional + Finalize func(ctx context.Context, resource Type) error // Finalize does whatever work is necessary for the reconciler when the resource is pending // deletion. If this reconciler sets a finalizer it should do the necessary work to clean up // state the finalizer represents and then clear the finalizer. // - // Expected function signature: - // func(ctx context.Context, resource client.Object) error - // func(ctx context.Context, resource client.Object) (ctrl.Result, error) + // Mutually exclusive with Finalize // // +optional - Finalize interface{} + FinalizeWithResult func(ctx context.Context, resource Type) (Result, error) } -func (r *SyncReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { +func (r *SyncReconciler[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { if r.Name == "" { r.Name = "SyncReconciler" } @@ -863,79 +831,35 @@ func (r *SyncReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, return r.Setup(ctx, mgr, bldr) } -func (r *SyncReconciler) validate(ctx context.Context) error { - // validate Sync function signature: - // func(ctx context.Context, resource client.Object) error - // func(ctx context.Context, resource client.Object) (ctrl.Result, error) - if r.Sync == nil { - return fmt.Errorf("SyncReconciler %q must implement Sync", r.Name) - } else { - castResourceType := RetrieveResourceType(ctx) - fn := reflect.TypeOf(r.Sync) - err := fmt.Errorf("SyncReconciler %q must implement Sync: func(context.Context, %s) error | func(context.Context, %s) (ctrl.Result, error), found: %s", r.Name, reflect.TypeOf(castResourceType), reflect.TypeOf(castResourceType), fn) - if fn.NumIn() != 2 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(1)) { - return err - } - switch fn.NumOut() { - case 1: - if !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(0)) { - return err - } - case 2: - if !reflect.TypeOf(ctrl.Result{}).AssignableTo(fn.Out(0)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(1)) { - return err - } - default: - return err - } +func (r *SyncReconciler[T]) validate(ctx context.Context) error { + // validate Sync and SyncWithResult + if r.Sync == nil && r.SyncWithResult == nil { + return fmt.Errorf("SyncReconciler %q must implement Sync or SyncWithResult", r.Name) + } + if r.Sync != nil && r.SyncWithResult != nil { + return fmt.Errorf("SyncReconciler %q may not implement both Sync and SyncWithResult", r.Name) } - // validate Finalize function signature: - // nil - // func(ctx context.Context, resource client.Object) error - // func(ctx context.Context, resource client.Object) (ctrl.Result, error) - if r.Finalize != nil { - castResourceType := RetrieveResourceType(ctx) - fn := reflect.TypeOf(r.Finalize) - err := fmt.Errorf("SyncReconciler %q must implement Finalize: nil | func(context.Context, %s) error | func(context.Context, %s) (ctrl.Result, error), found: %s", r.Name, reflect.TypeOf(castResourceType), reflect.TypeOf(castResourceType), fn) - if fn.NumIn() != 2 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(1)) { - return err - } - switch fn.NumOut() { - case 1: - if !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(0)) { - return err - } - case 2: - if !reflect.TypeOf(ctrl.Result{}).AssignableTo(fn.Out(0)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(1)) { - return err - } - default: - return err - } + // validate Finalize and FinalizeWithResult + if r.Finalize != nil && r.FinalizeWithResult != nil { + return fmt.Errorf("SyncReconciler %q may not implement both Finalize and FinalizeWithResult", r.Name) } return nil } -func (r *SyncReconciler) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *SyncReconciler[T]) Reconcile(ctx context.Context, resource T) (Result, error) { log := logr.FromContextOrDiscard(ctx). WithName(r.Name) ctx = logr.NewContext(ctx, log) - result := ctrl.Result{} + result := Result{} if resource.GetDeletionTimestamp() == nil || r.SyncDuringFinalization { syncResult, err := r.sync(ctx, resource) if err != nil { log.Error(err, "unable to sync") - return ctrl.Result{}, err + return Result{}, err } result = AggregateResults(result, syncResult) } @@ -944,7 +868,7 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, resource client.Object) finalizeResult, err := r.finalize(ctx, resource) if err != nil { log.Error(err, "unable to finalize") - return ctrl.Result{}, err + return Result{}, err } result = AggregateResults(result, finalizeResult) } @@ -952,52 +876,24 @@ func (r *SyncReconciler) Reconcile(ctx context.Context, resource client.Object) return result, nil } -func (r *SyncReconciler) sync(ctx context.Context, resource client.Object) (ctrl.Result, error) { - fn := reflect.ValueOf(r.Sync) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(resource), - }) - result := ctrl.Result{} - var err error - switch len(out) { - case 2: - result = out[0].Interface().(ctrl.Result) - if !out[1].IsNil() { - err = out[1].Interface().(error) - } - case 1: - if !out[0].IsNil() { - err = out[0].Interface().(error) - } +func (r *SyncReconciler[T]) sync(ctx context.Context, resource T) (Result, error) { + if r.Sync != nil { + err := r.Sync(ctx, resource) + return Result{}, err } - return result, err + return r.SyncWithResult(ctx, resource) } -func (r *SyncReconciler) finalize(ctx context.Context, resource client.Object) (ctrl.Result, error) { - if r.Finalize == nil { - return ctrl.Result{}, nil +func (r *SyncReconciler[T]) finalize(ctx context.Context, resource T) (Result, error) { + if r.Finalize != nil { + err := r.Finalize(ctx, resource) + return Result{}, err } - - fn := reflect.ValueOf(r.Finalize) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(resource), - }) - result := ctrl.Result{} - var err error - switch len(out) { - case 2: - result = out[0].Interface().(ctrl.Result) - if !out[1].IsNil() { - err = out[1].Interface().(error) - } - case 1: - if !out[0].IsNil() { - err = out[0].Interface().(error) - } + if r.FinalizeWithResult != nil { + return r.FinalizeWithResult(ctx, resource) } - return result, err + + return Result{}, nil } var ( @@ -1018,7 +914,7 @@ var ( // - ReflectChildStatusOnParent // // During setup, the child resource type is registered to watch for changes. -type ChildReconciler struct { +type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectList] struct { // Name used to identify this reconciler. Defaults to `{ChildType}ChildReconciler`. Ideally // unique, but not required to be so. // @@ -1026,11 +922,17 @@ type ChildReconciler struct { Name string // ChildType is the resource being created/updated/deleted by the reconciler. For example, a - // reconciled resource Deployment would have a ReplicaSet as a child. - ChildType client.Object + // reconciled resource Deployment would have a ReplicaSet as a child. Required when the + // generic type is not a struct, or is unstructured. + // + // +optional + ChildType ChildType // ChildListType is the listing type for the child type. For example, - // PodList is the list type for Pod - ChildListType client.ObjectList + // PodList is the list type for Pod. Required when the generic type is not + // a struct, or is unstructured. + // + // +optional + ChildListType ChildListType // 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 @@ -1064,40 +966,24 @@ type ChildReconciler struct { // // To skip reconciliation of the child resource while still reflecting an existing child's // status on the reconciled resource, return OnlyReconcileChildStatus as an error. - // - // Expected function signature: - // func(ctx context.Context, resource client.Object) (client.Object, error) - DesiredChild interface{} + DesiredChild func(ctx context.Context, resource Type) (ChildType, error) // ReflectChildStatusOnParent updates the reconciled resource's status with values from the // child. Select types of error are passed, including: // - apierrs.IsConflict - // - // Expected function signature: - // func(parent, child client.Object, err error) - ReflectChildStatusOnParent interface{} + ReflectChildStatusOnParent func(parent Type, child ChildType, err error) // 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. // - // Expected function signature: - // func(current, desired client.Object) - // // +optional - HarmonizeImmutableFields interface{} + HarmonizeImmutableFields func(current, desired ChildType) // MergeBeforeUpdate copies desired fields on to the current object before // calling update. Typically fields to copy are the Spec, Labels and // Annotations. - // - // Expected function signature: - // func(current, desired client.Object) - MergeBeforeUpdate interface{} - - // Deprecated SemanticEquals is no longer used, the field can be removed. Equality is - // now determined based on the resource mutated by MergeBeforeUpdate - SemanticEquals interface{} + MergeBeforeUpdate func(current, desired ChildType) // ListOptions allows custom options to be use when listing potential child resources. Each // resource retrieved as part of the listing is confirmed via OurChild. @@ -1107,11 +993,8 @@ type ChildReconciler struct { // client.InNamespace(resource.GetNamespace()), // } // - // Expected function signature: - // func(ctx context.Context, resource client.Object) []client.ListOption - // // +optional - ListOptions interface{} + ListOptions func(ctx context.Context, resource Type) []client.ListOption // OurChild is used when there are multiple ChildReconciler for the same ChildType controlled // by the same reconciled resource. The function return true for child resources managed by @@ -1120,32 +1003,49 @@ type ChildReconciler struct { // // OurChild is required when a Finalizer is defined or SkipOwnerReference is true. // - // Expected function signature: - // func(resource, child client.Object) bool - // // +optional - OurChild interface{} + OurChild func(resource Type, child ChildType) bool // 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. // - // Expected function signature: - // func(child client.Object) interface{} - // // +optional - Sanitize interface{} + Sanitize func(child ChildType) interface{} - stamp *ResourceManager + stamp *ResourceManager[ChildType] lazyInit sync.Once } -func (r *ChildReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { - c := RetrieveConfigOrDie(ctx) +func (r *ChildReconciler[T, CT, CLT]) init() { + r.lazyInit.Do(func() { + if internal.IsNil(r.ChildType) { + var nilCT CT + r.ChildType = newEmpty(nilCT).(CT) + } + if internal.IsNil(r.ChildListType) { + var nilCLT CLT + r.ChildListType = newEmpty(nilCLT).(CLT) + } + if r.Name == "" { + r.Name = fmt.Sprintf("%sChildReconciler", typeName(r.ChildType)) + } + r.stamp = &ResourceManager[CT]{ + Name: r.Name, + Type: r.ChildType, + Finalizer: r.Finalizer, + TrackDesired: r.SkipOwnerReference, + HarmonizeImmutableFields: r.HarmonizeImmutableFields, + MergeBeforeUpdate: r.MergeBeforeUpdate, + Sanitize: r.Sanitize, + } + }) +} - if r.Name == "" { - r.Name = fmt.Sprintf("%sChildReconciler", typeName(r.ChildType)) - } +func (r *ChildReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { + r.init() + + c := RetrieveConfigOrDie(ctx) log := logr.FromContextOrDiscard(ctx). WithName(r.Name). @@ -1168,78 +1068,22 @@ func (r *ChildReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager return r.Setup(ctx, mgr, bldr) } -func (r *ChildReconciler) validate(ctx context.Context) error { - castResourceType := RetrieveResourceType(ctx) - +func (r *ChildReconciler[T, CT, CLT]) validate(ctx context.Context) error { // default implicit values if r.Finalizer != "" { r.SkipOwnerReference = true } - // validate ChildType value - if r.ChildType == nil { - return fmt.Errorf("ChildReconciler %q must define ChildType", r.Name) - } - - // validate ChildListType value - if r.ChildListType == nil { - return fmt.Errorf("ChildReconciler %q must define ChildListType", r.Name) - } - - // validate DesiredChild function signature: - // func(ctx context.Context, resource client.Object) (client.Object, error) + // require DesiredChild if r.DesiredChild == nil { return fmt.Errorf("ChildReconciler %q must implement DesiredChild", r.Name) - } else { - fn := reflect.TypeOf(r.DesiredChild) - if fn.NumIn() != 2 || fn.NumOut() != 2 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(1)) || - !reflect.TypeOf(r.ChildType).AssignableTo(fn.Out(0)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.Out(1)) { - return fmt.Errorf("ChildReconciler %q must implement DesiredChild: func(context.Context, %s) (%s, error), found: %s", r.Name, reflect.TypeOf(castResourceType), reflect.TypeOf(r.ChildType), fn) - } } - // validate ReflectChildStatusOnParent function signature: - // func(resource, child client.Object, err error) + // require ReflectChildStatusOnParent if r.ReflectChildStatusOnParent == nil { return fmt.Errorf("ChildReconciler %q must implement ReflectChildStatusOnParent", r.Name) - } else { - fn := reflect.TypeOf(r.ReflectChildStatusOnParent) - if fn.NumIn() != 3 || fn.NumOut() != 0 || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(0)) || - !reflect.TypeOf(r.ChildType).AssignableTo(fn.In(1)) || - !reflect.TypeOf((*error)(nil)).Elem().AssignableTo(fn.In(2)) { - return fmt.Errorf("ChildReconciler %q must implement ReflectChildStatusOnParent: func(%s, %s, error), found: %s", r.Name, reflect.TypeOf(castResourceType), reflect.TypeOf(r.ChildType), fn) - } } - // validate ListOptions function signature: - // nil - // func(ctx context.Context, resource client.Object) []client.ListOption - if r.ListOptions != nil { - fn := reflect.TypeOf(r.ListOptions) - if fn.NumIn() != 2 || fn.NumOut() != 1 || - !reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(1)) || - !reflect.TypeOf([]client.ListOption{}).AssignableTo(fn.Out(0)) { - return fmt.Errorf("ChildReconciler %q must implement ListOptions: nil | func(context.Context, %s) []client.ListOption, found: %s", r.Name, reflect.TypeOf(castResourceType), fn) - } - } - - // validate OurChild function signature: - // nil - // func(resource, child client.Object) bool - if r.OurChild != nil { - fn := reflect.TypeOf(r.OurChild) - if fn.NumIn() != 2 || fn.NumOut() != 1 || - !reflect.TypeOf(castResourceType).AssignableTo(fn.In(0)) || - !reflect.TypeOf(r.ChildType).AssignableTo(fn.In(1)) || - fn.Out(0).Kind() != reflect.Bool { - return fmt.Errorf("ChildReconciler %q must implement OurChild: nil | func(%s, %s) bool, found: %s", r.Name, reflect.TypeOf(castResourceType), reflect.TypeOf(r.ChildType), fn) - } - } if r.OurChild == nil && r.SkipOwnerReference { // OurChild is required when SkipOwnerReference is true return fmt.Errorf("ChildReconciler %q must implement OurChild since owner references are not used", r.Name) @@ -1248,7 +1092,9 @@ func (r *ChildReconciler) validate(ctx context.Context) error { return nil } -func (r *ChildReconciler) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *ChildReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T) (Result, error) { + r.init() + c := RetrieveConfigOrDie(ctx) log := logr.FromContextOrDiscard(ctx). @@ -1256,21 +1102,9 @@ func (r *ChildReconciler) Reconcile(ctx context.Context, resource client.Object) WithValues("childType", gvk(r.ChildType, c.Scheme())) ctx = logr.NewContext(ctx, log) - r.lazyInit.Do(func() { - r.stamp = &ResourceManager{ - Name: r.Name, - Type: r.ChildType, - Finalizer: r.Finalizer, - TrackDesired: r.SkipOwnerReference, - HarmonizeImmutableFields: r.HarmonizeImmutableFields, - MergeBeforeUpdate: r.MergeBeforeUpdate, - Sanitize: r.Sanitize, - } - }) - child, err := r.reconcile(ctx, resource) if resource.GetDeletionTimestamp() != nil { - return ctrl.Result{}, err + return Result{}, err } if err != nil { if apierrs.IsAlreadyExists(err) { @@ -1278,33 +1112,34 @@ func (r *ChildReconciler) Reconcile(ctx context.Context, resource client.Object) // the created child from a previous turn may be slow to appear in the informer cache, but shouldn't appear // on the reconciled resource as being not ready. apierr := err.(apierrs.APIStatus) - conflicted := newEmpty(r.ChildType).(client.Object) + conflicted := newEmpty(r.ChildType).(CT) _ = c.APIReader.Get(ctx, types.NamespacedName{Namespace: resource.GetNamespace(), Name: apierr.Status().Details.Name}, conflicted) if r.ourChild(resource, conflicted) { // skip updating the reconciled resource's status, fail and try again - return ctrl.Result{}, err + return Result{}, err } log.Info("unable to reconcile child, not owned", "child", namespaceName(conflicted), "ownerRefs", conflicted.GetOwnerReferences()) - r.reflectChildStatusOnParent(resource, child, err) - return ctrl.Result{}, nil + r.ReflectChildStatusOnParent(resource, child, err) + return Result{}, nil } log.Error(err, "unable to reconcile child") - return ctrl.Result{}, err + return Result{}, err } - r.reflectChildStatusOnParent(resource, child, err) + r.ReflectChildStatusOnParent(resource, child, err) - return ctrl.Result{}, nil + return Result{}, nil } -func (r *ChildReconciler) reconcile(ctx context.Context, resource client.Object) (client.Object, error) { +func (r *ChildReconciler[T, CT, CLT]) reconcile(ctx context.Context, resource T) (CT, error) { + var nilCT CT log := logr.FromContextOrDiscard(ctx) pc := RetrieveOriginalConfigOrDie(ctx) c := RetrieveConfigOrDie(ctx) - actual := newEmpty(r.ChildType).(client.Object) - children := newEmpty(r.ChildListType).(client.ObjectList) + actual := newEmpty(r.ChildType).(CT) + children := newEmpty(r.ChildListType).(CLT) if err := c.List(ctx, children, r.listOptions(ctx, resource)...); err != nil { - return nil, err + return nilCT, err } items := r.filterChildren(resource, children) if len(items) == 1 { @@ -1316,7 +1151,7 @@ func (r *ChildReconciler) reconcile(ctx context.Context, resource client.Object) if err := c.Delete(ctx, extra); err != nil { pc.Recorder.Eventf(resource, corev1.EventTypeWarning, "DeleteFailed", "Failed to delete %s %q: %v", typeName(r.ChildType), extra.GetName(), err) - return nil, err + return nilCT, err } pc.Recorder.Eventf(resource, corev1.EventTypeNormal, "Deleted", "Deleted %s %q", typeName(r.ChildType), extra.GetName()) @@ -1328,12 +1163,12 @@ func (r *ChildReconciler) reconcile(ctx context.Context, resource client.Object) if errors.Is(err, OnlyReconcileChildStatus) { return actual, nil } - return nil, err + return nilCT, err } - if desired != nil { + if !internal.IsNil(desired) { if !r.SkipOwnerReference { if err := ctrl.SetControllerReference(resource, desired, c.Scheme()); err != nil { - return nil, err + return nilCT, err } } if !r.ourChild(resource, desired) { @@ -1345,53 +1180,23 @@ func (r *ChildReconciler) reconcile(ctx context.Context, resource client.Object) return r.stamp.Manage(ctx, resource, actual, desired) } -func (r *ChildReconciler) desiredChild(ctx context.Context, resource client.Object) (client.Object, error) { +func (r *ChildReconciler[T, CT, CLT]) desiredChild(ctx context.Context, resource T) (CT, error) { + var nilCT CT + if resource.GetDeletionTimestamp() != nil { // the reconciled resource is pending deletion, cleanup the child resource - return nil, nil - } - - fn := reflect.ValueOf(r.DesiredChild) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(resource), - }) - var obj client.Object - if !out[0].IsNil() { - obj = out[0].Interface().(client.Object) - } - var err error - if !out[1].IsNil() { - err = out[1].Interface().(error) + return nilCT, nil } - return obj, err -} -func (r *ChildReconciler) reflectChildStatusOnParent(resource, child client.Object, err error) { - fn := reflect.ValueOf(r.ReflectChildStatusOnParent) - args := []reflect.Value{ - reflect.ValueOf(resource), - reflect.ValueOf(child), - reflect.ValueOf(err), - } - if resource == nil { - args[0] = reflect.New(fn.Type().In(0)).Elem() - } - if child == nil { - args[1] = reflect.New(fn.Type().In(1)).Elem() - } - if err == nil { - args[2] = reflect.New(fn.Type().In(2)).Elem() - } - fn.Call(args) + return r.DesiredChild(ctx, resource) } -func (r *ChildReconciler) filterChildren(resource client.Object, children client.ObjectList) []client.Object { +func (r *ChildReconciler[T, CT, CLT]) filterChildren(resource T, children CLT) []CT { childrenValue := reflect.ValueOf(children).Elem() itemsValue := childrenValue.FieldByName("Items") - items := []client.Object{} + items := []CT{} for i := 0; i < itemsValue.Len(); i++ { - obj := itemsValue.Index(i).Addr().Interface().(client.Object) + obj := itemsValue.Index(i).Addr().Interface().(CT) if r.ourChild(resource, obj) { items = append(items, obj) } @@ -1399,21 +1204,16 @@ func (r *ChildReconciler) filterChildren(resource client.Object, children client return items } -func (r *ChildReconciler) listOptions(ctx context.Context, resource client.Object) []client.ListOption { +func (r *ChildReconciler[T, CT, CLT]) listOptions(ctx context.Context, resource T) []client.ListOption { if r.ListOptions == nil { return []client.ListOption{ client.InNamespace(resource.GetNamespace()), } } - fn := reflect.ValueOf(r.ListOptions) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(ctx), - reflect.ValueOf(resource), - }) - return out[0].Interface().([]client.ListOption) + return r.ListOptions(ctx, resource) } -func (r *ChildReconciler) ourChild(resource, obj client.Object) bool { +func (r *ChildReconciler[T, CT, CLT]) ourChild(resource T, obj CT) bool { if !r.SkipOwnerReference && !metav1.IsControlledBy(obj, resource) { return false } @@ -1421,23 +1221,14 @@ func (r *ChildReconciler) ourChild(resource, obj client.Object) bool { if r.OurChild == nil { return true } - fn := reflect.ValueOf(r.OurChild) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(resource), - reflect.ValueOf(obj), - }) - keep := true - if out[0].Kind() == reflect.Bool { - keep = out[0].Bool() - } - return keep + return r.OurChild(resource, obj) } // Sequence is a collection of SubReconcilers called in order. If a // reconciler errs, further reconcilers are skipped. -type Sequence []SubReconciler +type Sequence[Type client.Object] []SubReconciler[Type] -func (r Sequence) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { +func (r Sequence[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { for i, reconciler := range r { log := logr.FromContextOrDiscard(ctx). WithName(fmt.Sprintf("%d", i)) @@ -1451,8 +1242,8 @@ func (r Sequence) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr * return nil } -func (r Sequence) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { - aggregateResult := ctrl.Result{} +func (r Sequence[T]) Reconcile(ctx context.Context, resource T) (Result, error) { + aggregateResult := Result{} for i, reconciler := range r { log := logr.FromContextOrDiscard(ctx). WithName(fmt.Sprintf("%d", i)) @@ -1460,7 +1251,7 @@ func (r Sequence) Reconcile(ctx context.Context, resource client.Object) (ctrl.R result, err := reconciler.Reconcile(ctx, resource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } aggregateResult = AggregateResults(result, aggregateResult) } @@ -1468,50 +1259,64 @@ func (r Sequence) Reconcile(ctx context.Context, resource client.Object) (ctrl.R return aggregateResult, nil } -// Deprecated use CastResource -type CastParent = CastResource - // CastResource casts the ResourceReconciler's type by projecting the resource data // onto a new struct. Casting the reconciled resource is useful to create cross // cutting reconcilers that can operate on common portion of multiple resources, // commonly referred to as a duck type. -type CastResource struct { +// +// If the CastType generic is an interface rather than a struct, the resource is +// passed directly rather than converted. +type CastResource[Type, CastType client.Object] struct { // Name used to identify this reconciler. Defaults to `{Type}CastResource`. Ideally unique, but // not required to be so. // // +optional Name string - // Type of resource to reconcile - Type client.Object - // Reconciler is called for each reconciler request with the reconciled resource. Typically a // Sequence is used to compose multiple SubReconcilers. - Reconciler SubReconciler + Reconciler SubReconciler[CastType] + + noop bool + lazyInit sync.Once } -func (r *CastResource) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { - if r.Name == "" { - r.Name = fmt.Sprintf("%sCastResource", typeName(r.Type)) - } +func (r *CastResource[T, CT]) init() { + r.lazyInit.Do(func() { + var nilCT CT + if reflect.ValueOf(nilCT).Kind() == reflect.Invalid { + // not a real cast, just converting generic types + r.noop = true + return + } + emptyCT := newEmpty(nilCT) + if r.Name == "" { + r.Name = fmt.Sprintf("%sCastResource", typeName(emptyCT)) + } + }) +} - log := logr.FromContextOrDiscard(ctx). - WithName(r.Name). - WithValues("castResourceType", typeName(r.Type)) - ctx = logr.NewContext(ctx, log) +func (r *CastResource[T, CT]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { + r.init() - if err := r.validate(ctx); err != nil { - return err + if !r.noop { + var nilCT CT + emptyCT := newEmpty(nilCT).(CT) + + log := logr.FromContextOrDiscard(ctx). + WithName(r.Name). + WithValues("castResourceType", typeName(emptyCT)) + ctx = logr.NewContext(ctx, log) + + if err := r.validate(ctx); err != nil { + return err + } } + return r.Reconciler.SetupWithManager(ctx, mgr, bldr) } -func (r *CastResource) validate(ctx context.Context) error { - // validate Type value - if r.Type == nil { - return fmt.Errorf("CastResource %q must define Type", r.Name) - } - +func (r *CastResource[T, CT]) validate(ctx context.Context) error { // validate Reconciler value if r.Reconciler == nil { return fmt.Errorf("CastResource %q must define Reconciler", r.Name) @@ -1520,45 +1325,57 @@ func (r *CastResource) validate(ctx context.Context) error { return nil } -func (r *CastResource) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *CastResource[T, CT]) Reconcile(ctx context.Context, resource T) (Result, error) { + r.init() + + if r.noop { + // cast the type rather than convert the object + return r.Reconciler.Reconcile(ctx, client.Object(resource).(CT)) + } + + var nilCT CT + emptyCT := newEmpty(nilCT).(CT) + log := logr.FromContextOrDiscard(ctx). WithName(r.Name). - WithValues("castResourceType", typeName(r.Type)) + WithValues("castResourceType", typeName(emptyCT)) ctx = logr.NewContext(ctx, log) ctx, castResource, err := r.cast(ctx, resource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } castOriginal := castResource.DeepCopyObject().(client.Object) result, err := r.Reconciler.Reconcile(ctx, castResource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } if !equality.Semantic.DeepEqual(castResource, castOriginal) { // patch the reconciled resource with the updated duck values patch, err := NewPatch(castOriginal, castResource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } err = patch.Apply(resource) if err != nil { - return ctrl.Result{}, err + return Result{}, err } } return result, nil } -func (r *CastResource) cast(ctx context.Context, resource client.Object) (context.Context, client.Object, error) { +func (r *CastResource[T, CT]) cast(ctx context.Context, resource T) (context.Context, CT, error) { + var nilCT CT + data, err := json.Marshal(resource) if err != nil { - return nil, nil, err + return nil, nilCT, err } - castResource := newEmpty(r.Type).(client.Object) + castResource := newEmpty(nilCT).(CT) err = json.Unmarshal(data, castResource) if err != nil { - return nil, nil, err + return nil, nilCT, err } if kind := castResource.GetObjectKind(); kind.GroupVersionKind().Empty() { // default the apiVersion/kind with the real value from the resource if not already defined @@ -1575,7 +1392,7 @@ func (r *CastResource) cast(ctx context.Context, resource client.Object) (contex // // The specified config can be accessed with `RetrieveConfig(ctx)`, the original config used to // load the reconciled resource can be accessed with `RetrieveOriginalConfig(ctx)`. -type WithConfig struct { +type WithConfig[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `WithConfig`. Ideally unique, but // not required to be so. // @@ -1590,10 +1407,10 @@ type WithConfig struct { // Reconciler is called for each reconciler request with the reconciled // resource being reconciled. Typically a Sequence is used to compose // multiple SubReconcilers. - Reconciler SubReconciler + Reconciler SubReconciler[Type] } -func (r *WithConfig) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { +func (r *WithConfig[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { if r.Name == "" { r.Name = "WithConfig" } @@ -1613,7 +1430,7 @@ func (r *WithConfig) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bld return r.Reconciler.SetupWithManager(ctx, mgr, bldr) } -func (r *WithConfig) validate(ctx context.Context) error { +func (r *WithConfig[T]) validate(ctx context.Context) error { // validate Config value if r.Config == nil { return fmt.Errorf("WithConfig %q must define Config", r.Name) @@ -1627,14 +1444,14 @@ func (r *WithConfig) validate(ctx context.Context) error { return nil } -func (r *WithConfig) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *WithConfig[T]) Reconcile(ctx context.Context, resource T) (Result, error) { log := logr.FromContextOrDiscard(ctx). WithName(r.Name) ctx = logr.NewContext(ctx, log) c, err := r.Config(ctx, RetrieveConfigOrDie(ctx)) if err != nil { - return ctrl.Result{}, err + return Result{}, err } ctx = StashConfig(ctx, c) return r.Reconciler.Reconcile(ctx, resource) @@ -1644,7 +1461,7 @@ func (r *WithConfig) Reconcile(ctx context.Context, resource client.Object) (ctr // can be cleaned up upon the resource being deleted. The finalizer is added to the resource, if not // already set, before calling the nested reconciler. When the resource is terminating, the // finalizer is cleared after returning from the nested reconciler without error. -type WithFinalizer struct { +type WithFinalizer[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `WithFinalizer`. Ideally unique, but // not required to be so. // @@ -1662,10 +1479,10 @@ type WithFinalizer struct { // Reconciler is called for each reconciler request with the reconciled // resource being reconciled. Typically a Sequence is used to compose // multiple SubReconcilers. - Reconciler SubReconciler + Reconciler SubReconciler[Type] } -func (r *WithFinalizer) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { +func (r *WithFinalizer[T]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error { if r.Name == "" { r.Name = "WithFinalizer" } @@ -1680,7 +1497,7 @@ func (r *WithFinalizer) SetupWithManager(ctx context.Context, mgr ctrl.Manager, return r.Reconciler.SetupWithManager(ctx, mgr, bldr) } -func (r *WithFinalizer) validate(ctx context.Context) error { +func (r *WithFinalizer[T]) validate(ctx context.Context) error { // validate Finalizer value if r.Finalizer == "" { return fmt.Errorf("WithFinalizer %q must define Finalizer", r.Name) @@ -1694,14 +1511,14 @@ func (r *WithFinalizer) validate(ctx context.Context) error { return nil } -func (r *WithFinalizer) Reconcile(ctx context.Context, resource client.Object) (ctrl.Result, error) { +func (r *WithFinalizer[T]) Reconcile(ctx context.Context, resource T) (Result, error) { log := logr.FromContextOrDiscard(ctx). WithName(r.Name) ctx = logr.NewContext(ctx, log) if resource.GetDeletionTimestamp() == nil { if err := AddFinalizer(ctx, resource, r.Finalizer); err != nil { - return ctrl.Result{}, err + return Result{}, err } } result, err := r.Reconciler.Reconcile(ctx, resource) @@ -1710,22 +1527,25 @@ func (r *WithFinalizer) Reconcile(ctx context.Context, resource client.Object) ( } if resource.GetDeletionTimestamp() != nil { if err := ClearFinalizer(ctx, resource, r.Finalizer); err != nil { - return ctrl.Result{}, err + return Result{}, err } } return result, err } // ResourceManager compares the actual and desired resources to create/update/delete as desired. -type ResourceManager struct { +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. - Type client.Object + // 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 @@ -1747,33 +1567,20 @@ type ResourceManager struct { // object to be copied to the desired object in order to avoid creating // updates which are guaranteed to fail. // - // Expected function signature: - // func(current, desired client.Object) - // // +optional - HarmonizeImmutableFields interface{} + 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. - // - // Expected function signature: - // func(current, desired client.Object) - MergeBeforeUpdate interface{} - - // Deprecated SemanticEquals is no longer used, the field can be removed. Equality is - // now determined based on the resource mutated by MergeBeforeUpdate - SemanticEquals interface{} + 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. // - // Expected function signature: - // func(child client.Object) interface{} - // // +optional - Sanitize interface{} + Sanitize func(child Type) interface{} // mutationCache holds patches received from updates to a resource made by // mutation webhooks. This cache is used to avoid unnecessary update calls @@ -1782,54 +1589,28 @@ type ResourceManager struct { lazyInit sync.Once } -func (r *ResourceManager) Setup(ctx context.Context) error { - if r.Name == "" { - r.Name = fmt.Sprintf("%sResourceManager", typeName(r.Type)) - } +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.mutationCache = cache.NewExpiring() + }) +} +func (r *ResourceManager[T]) Setup(ctx context.Context) error { + r.init() return r.validate(ctx) } -func (r *ResourceManager) validate(ctx context.Context) error { - // validate Type value - if r.Type == nil { - return fmt.Errorf("ResourceManager %q must define Type", r.Name) - } - - // validate HarmonizeImmutableFields function signature: - // nil - // func(current, desired client.Object) - if r.HarmonizeImmutableFields != nil { - fn := reflect.TypeOf(r.HarmonizeImmutableFields) - if fn.NumIn() != 2 || fn.NumOut() != 0 || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(0)) || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(1)) { - return fmt.Errorf("ResourceManager %q must implement HarmonizeImmutableFields: nil | func(%s, %s), found: %s", r.Name, reflect.TypeOf(r.Type), reflect.TypeOf(r.Type), fn) - } - } - - // validate MergeBeforeUpdate function signature: - // func(current, desired client.Object) +func (r *ResourceManager[T]) validate(ctx context.Context) error { + // require MergeBeforeUpdate if r.MergeBeforeUpdate == nil { return fmt.Errorf("ResourceManager %q must define MergeBeforeUpdate", r.Name) - } else { - fn := reflect.TypeOf(r.MergeBeforeUpdate) - if fn.NumIn() != 2 || fn.NumOut() != 0 || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(0)) || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(1)) { - return fmt.Errorf("ResourceManager %q must implement MergeBeforeUpdate: func(%s, %s), found: %s", r.Name, reflect.TypeOf(r.Type), reflect.TypeOf(r.Type), fn) - } - } - - // validate Sanitize function signature: - // nil - // func(child client.Object) interface{} - if r.Sanitize != nil { - fn := reflect.TypeOf(r.Sanitize) - if fn.NumIn() != 1 || fn.NumOut() != 1 || - !reflect.TypeOf(r.Type).AssignableTo(fn.In(0)) { - return fmt.Errorf("ResourceManager %q must implement Sanitize: nil | func(%s) interface{}, found: %s", r.Name, reflect.TypeOf(r.Type), fn) - } } return nil @@ -1838,41 +1619,41 @@ func (r *ResourceManager) 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) Manage(ctx context.Context, resource, actual, desired client.Object) (client.Object, error) { +func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, actual, desired T) (T, error) { + r.init() + + var nilT T + log := logr.FromContextOrDiscard(ctx) pc := RetrieveOriginalConfigOrDie(ctx) c := RetrieveConfigOrDie(ctx) - r.lazyInit.Do(func() { - r.mutationCache = cache.NewExpiring() - }) - - if (actual == nil || actual.GetCreationTimestamp().Time.IsZero()) && desired == nil { + if (internal.IsNil(actual) || actual.GetCreationTimestamp().Time.IsZero()) && internal.IsNil(desired) { if err := ClearFinalizer(ctx, resource, r.Finalizer); err != nil { - return nil, err + return nilT, err } - return nil, nil + return nilT, nil } // delete resource if no longer needed - if desired == nil { + if internal.IsNil(desired) { if !actual.GetCreationTimestamp().Time.IsZero() && actual.GetDeletionTimestamp() == nil { log.Info("deleting unwanted resource", "resource", namespaceName(actual)) if err := c.Delete(ctx, actual); err != nil { log.Error(err, "unable to delete unwanted resource", "resource", namespaceName(actual)) pc.Recorder.Eventf(resource, corev1.EventTypeWarning, "DeleteFailed", "Failed to delete %s %q: %v", typeName(actual), actual.GetName(), err) - return nil, err + return nilT, err } pc.Recorder.Eventf(resource, corev1.EventTypeNormal, "Deleted", "Deleted %s %q", typeName(actual), actual.GetName()) } - return nil, nil + return nilT, nil } if err := AddFinalizer(ctx, resource, r.Finalizer); err != nil { - return nil, err + return nilT, err } // create resource if it doesn't exist @@ -1882,13 +1663,13 @@ func (r *ResourceManager) Manage(ctx context.Context, resource, actual, desired log.Error(err, "unable to create resource", "resource", namespaceName(desired)) pc.Recorder.Eventf(resource, corev1.EventTypeWarning, "CreationFailed", "Failed to create %s %q: %v", typeName(desired), desired.GetName(), err) - return nil, err + return nilT, err } if r.TrackDesired { // normally tracks should occur before API operations, but when creating a resource with a // generated name, we need to know the actual resource name. if err := c.Tracker.TrackChild(ctx, resource, desired, c.Scheme()); err != nil { - return nil, err + return nilT, err } } pc.Recorder.Eventf(resource, corev1.EventTypeNormal, "Created", @@ -1897,10 +1678,12 @@ func (r *ResourceManager) Manage(ctx context.Context, resource, actual, desired } // overwrite fields that should not be mutated - r.harmonizeImmutableFields(actual, desired) + if r.HarmonizeImmutableFields != nil { + r.HarmonizeImmutableFields(actual, desired) + } // lookup and apply remote mutations - desiredPatched := desired.DeepCopyObject().(client.Object) + desiredPatched := desired.DeepCopyObject().(T) if patch, ok := r.mutationCache.Get(actual.GetUID()); ok { // the only object added to the cache is *Patch err := patch.(*Patch).Apply(desiredPatched) @@ -1911,8 +1694,8 @@ func (r *ResourceManager) Manage(ctx context.Context, resource, actual, desired } // update resource with desired changes - current := actual.DeepCopyObject().(client.Object) - r.mergeBeforeUpdate(current, desiredPatched) + current := actual.DeepCopyObject().(T) + r.MergeBeforeUpdate(current, desiredPatched) if equality.Semantic.DeepEqual(current, actual) { // resource is unchanged log.Info("resource is in sync, no update required") @@ -1921,19 +1704,19 @@ func (r *ResourceManager) Manage(ctx context.Context, resource, actual, desired log.Info("updating resource", "diff", cmp.Diff(r.sanitize(actual), r.sanitize(current))) if r.TrackDesired { if err := c.Tracker.TrackChild(ctx, resource, current, c.Scheme()); err != nil { - return nil, err + return nilT, err } } if err := c.Update(ctx, current); err != nil { log.Error(err, "unable to update resource", "resource", namespaceName(current)) pc.Recorder.Eventf(resource, corev1.EventTypeWarning, "UpdateFailed", "Failed to update %s %q: %v", typeName(current), current.GetName(), err) - return nil, err + return nilT, err } // capture admission mutation patch - base := current.DeepCopyObject().(client.Object) - r.mergeBeforeUpdate(base, desired) + base := current.DeepCopyObject().(T) + r.MergeBeforeUpdate(base, desired) patch, err := NewPatch(base, current) if err != nil { log.Error(err, "unable to generate mutation patch", "snapshot", r.sanitize(desired), "base", r.sanitize(base)) @@ -1948,45 +1731,17 @@ func (r *ResourceManager) Manage(ctx context.Context, resource, actual, desired return current, nil } -func (r *ResourceManager) harmonizeImmutableFields(current, desired client.Object) { - if r.HarmonizeImmutableFields == nil { - return - } - fn := reflect.ValueOf(r.HarmonizeImmutableFields) - fn.Call([]reflect.Value{ - reflect.ValueOf(current), - reflect.ValueOf(desired), - }) -} - -func (r *ResourceManager) mergeBeforeUpdate(current, desired client.Object) { - fn := reflect.ValueOf(r.MergeBeforeUpdate) - fn.Call([]reflect.Value{ - reflect.ValueOf(current), - reflect.ValueOf(desired), - }) -} - -func (r *ResourceManager) sanitize(resource client.Object) interface{} { +func (r *ResourceManager[T]) sanitize(resource T) interface{} { if r.Sanitize == nil { return resource } - if resource == nil { + if internal.IsNil(resource) { return nil } // avoid accidental mutations in Sanitize method - resource = resource.DeepCopyObject().(client.Object) - - fn := reflect.ValueOf(r.Sanitize) - out := fn.Call([]reflect.Value{ - reflect.ValueOf(resource), - }) - var sanitized interface{} - if !out[0].IsNil() { - sanitized = out[0].Interface() - } - return sanitized + resource = resource.DeepCopyObject().(T) + return r.Sanitize(resource) } func typeName(i interface{}) string { @@ -2048,9 +1803,8 @@ func ensureFinalizer(ctx context.Context, resource client.Object, finalizer stri } // cast the current object back to the resource so scheme-aware, typed client can operate on it - cast := &CastResource{ - Type: resourceType, - Reconciler: &SyncReconciler{ + cast := &CastResource[client.Object, client.Object]{ + Reconciler: &SyncReconciler[client.Object]{ SyncDuringFinalization: true, Sync: func(ctx context.Context, current client.Object) error { log := logr.FromContextOrDiscard(ctx) @@ -2090,8 +1844,8 @@ func ensureFinalizer(ctx context.Context, resource client.Object, finalizer stri // AggregateResults combines multiple results into a single result. If any result requests // requeue, the aggregate is requeued. The shortest non-zero requeue after is the aggregate value. -func AggregateResults(results ...ctrl.Result) ctrl.Result { - aggregate := ctrl.Result{} +func AggregateResults(results ...Result) Result { + aggregate := Result{} for _, result := range results { if result.RequeueAfter != 0 && (aggregate.RequeueAfter == 0 || result.RequeueAfter < aggregate.RequeueAfter) { aggregate.RequeueAfter = result.RequeueAfter diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index a7ce3d4..6464ffa 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -29,8 +29,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - controllerruntime "sigs.k8s.io/controller-runtime" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -56,9 +54,9 @@ func TestConfig_TrackAndGet(t *testing.T) { }). AddData("greeting", "hello") - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "track and get": { - Resource: resource, + Resource: resource.DieReleasePtr(), GivenObjects: []client.Object{ configMap, }, @@ -67,7 +65,7 @@ func TestConfig_TrackAndGet(t *testing.T) { }, }, "track with not found get": { - Resource: resource, + Resource: resource.DieReleasePtr(), ShouldErr: true, ExpectTracks: []rtesting.TrackRequest{ rtesting.NewTrackRequest(configMap, resource, scheme), @@ -77,8 +75,8 @@ func TestConfig_TrackAndGet(t *testing.T) { // run with typed objects t.Run("typed", func(t *testing.T) { - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c := reconcilers.RetrieveConfigOrDie(ctx) @@ -100,8 +98,8 @@ func TestConfig_TrackAndGet(t *testing.T) { // run with unstructured objects t.Run("unstructured", func(t *testing.T) { - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c := reconcilers.RetrieveConfigOrDie(ctx) @@ -127,7 +125,7 @@ func TestConfig_TrackAndGet(t *testing.T) { func TestResourceReconciler_NoStatus(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource-no-status" - testRequest := controllerruntime.Request{ + testRequest := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -148,8 +146,8 @@ func TestResourceReconciler_NoStatus(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNoStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceNoStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceNoStatus) error { return nil }, @@ -159,9 +157,8 @@ func TestResourceReconciler_NoStatus(t *testing.T) { }, } rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ResourceReconciler{ - Type: &resources.TestResourceNoStatus{}, - Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + return &reconcilers.ResourceReconciler[*resources.TestResourceNoStatus]{ + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNoStatus])(t, c), Config: c, } }) @@ -170,7 +167,7 @@ func TestResourceReconciler_NoStatus(t *testing.T) { func TestResourceReconciler_EmptyStatus(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource-empty-status" - testRequest := controllerruntime.Request{ + testRequest := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -191,8 +188,8 @@ func TestResourceReconciler_EmptyStatus(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceEmptyStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceEmptyStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceEmptyStatus) error { return nil }, @@ -202,9 +199,8 @@ func TestResourceReconciler_EmptyStatus(t *testing.T) { }, } rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ResourceReconciler{ - Type: &resources.TestResourceEmptyStatus{}, - Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + return &reconcilers.ResourceReconciler[*resources.TestResourceEmptyStatus]{ + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceEmptyStatus])(t, c), Config: c, } }) @@ -213,7 +209,7 @@ func TestResourceReconciler_EmptyStatus(t *testing.T) { func TestResourceReconciler_NilableStatus(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource" - testRequest := controllerruntime.Request{ + testRequest := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -238,8 +234,8 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { resource.Status(nil), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNilableStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceNilableStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceNilableStatus) error { if resource.Status != nil { t.Errorf("status expected to be nil") @@ -258,8 +254,8 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNilableStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceNilableStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceNilableStatus) error { expected := []metav1.Condition{ {Type: apis.ConditionReady, Status: metav1.ConditionUnknown, Reason: "Initializing"}, @@ -286,8 +282,8 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNilableStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceNilableStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceNilableStatus) error { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} @@ -319,8 +315,8 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNilableStatus] { + return &reconcilers.SyncReconciler[*resources.TestResourceNilableStatus]{ Sync: func(ctx context.Context, resource *resources.TestResourceNilableStatus) error { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} @@ -345,9 +341,8 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { } rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ResourceReconciler{ - Type: &resources.TestResourceNilableStatus{}, - Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + return &reconcilers.ResourceReconciler[*resources.TestResourceNilableStatus]{ + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResourceNilableStatus])(t, c), Config: c, } }) @@ -356,7 +351,7 @@ func TestResourceReconciler_NilableStatus(t *testing.T) { func TestResourceReconciler_Unstructured(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource" - testRequest := controllerruntime.Request{ + testRequest := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -384,8 +379,8 @@ func TestResourceReconciler_Unstructured(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*unstructured.Unstructured] { + return &reconcilers.SyncReconciler[*unstructured.Unstructured]{ Sync: func(ctx context.Context, resource *unstructured.Unstructured) error { return nil }, @@ -399,8 +394,8 @@ func TestResourceReconciler_Unstructured(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*unstructured.Unstructured] { + return &reconcilers.SyncReconciler[*unstructured.Unstructured]{ Sync: func(ctx context.Context, resource *unstructured.Unstructured) error { resource.Object["status"].(map[string]interface{})["fields"] = map[string]interface{}{ "Reconciler": "ran", @@ -422,14 +417,14 @@ func TestResourceReconciler_Unstructured(t *testing.T) { } rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ResourceReconciler{ + return &reconcilers.ResourceReconciler[*unstructured.Unstructured]{ Type: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": resources.GroupVersion.Identifier(), "kind": "TestResource", }, }, - Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*unstructured.Unstructured])(t, c), Config: c, } }) @@ -438,7 +433,7 @@ func TestResourceReconciler_Unstructured(t *testing.T) { func TestResourceReconciler(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource" - testRequest := controllerruntime.Request{ + testRequest := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -461,8 +456,8 @@ func TestResourceReconciler(t *testing.T) { "resource does not exist": { Request: testRequest, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { t.Error("should not be called") return nil @@ -479,8 +474,8 @@ func TestResourceReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { t.Error("should not be called") return nil @@ -498,8 +493,8 @@ func TestResourceReconciler(t *testing.T) { rtesting.InduceFailure("get", "TestResource"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { t.Error("should not be called") return nil @@ -515,8 +510,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if expected, actual := "ran", resource.Spec.Fields["Defaulter"]; expected != actual { t.Errorf("unexpected default value, actually = %v, expected = %v", expected, actual) @@ -535,8 +530,8 @@ func TestResourceReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { expected := []metav1.Condition{ {Type: apis.ConditionReady, Status: metav1.ConditionUnknown, Reason: "Initializing"}, @@ -563,8 +558,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} @@ -591,8 +586,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return fmt.Errorf("reconciler error") }, @@ -607,9 +602,9 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { resource.Status.Fields = map[string]string{ "want": "this to run", @@ -617,7 +612,7 @@ func TestResourceReconciler(t *testing.T) { return reconcilers.HaltSubReconcilers }, }, - &reconcilers.SyncReconciler{ + &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { resource.Status.Fields = map[string]string{ "don't want": "this to run", @@ -649,8 +644,8 @@ func TestResourceReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if resource.Status.Fields == nil { resource.Status.Fields = map[string]string{} @@ -678,8 +673,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { var key reconcilers.StashKey = "foo" // StashValue will panic if the context is not setup correctly @@ -696,8 +691,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if config := reconcilers.RetrieveConfigOrDie(ctx); config != c { t.Errorf("expected config in context, found %#v", config) @@ -717,8 +712,8 @@ func TestResourceReconciler(t *testing.T) { resource, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if resourceType, ok := reconcilers.RetrieveOriginalResourceType(ctx).(*resources.TestResource); !ok { t.Errorf("expected original resource type not in context, found %#v", resourceType) @@ -742,8 +737,8 @@ func TestResourceReconciler(t *testing.T) { value := "test-value" ctx = context.WithValue(ctx, key, value) - tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if v := ctx.Value(key); v != value { t.Errorf("expected %s to be in context", key) @@ -765,9 +760,8 @@ func TestResourceReconciler(t *testing.T) { } rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler { - return &reconcilers.ResourceReconciler{ - Type: &resources.TestResource{}, - Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + return &reconcilers.ResourceReconciler[*resources.TestResource]{ + Reconciler: rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c), Config: c, } }) @@ -776,7 +770,7 @@ func TestResourceReconciler(t *testing.T) { func TestAggregateReconciler(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource" - request := controllerruntime.Request{ + request := reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: testName}, } @@ -795,9 +789,8 @@ func TestAggregateReconciler(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { }) - defaultAggregateReconciler := func(c reconcilers.Config) *reconcilers.AggregateReconciler { - return &reconcilers.AggregateReconciler{ - Type: &corev1.ConfigMap{}, + defaultAggregateReconciler := func(c reconcilers.Config) *reconcilers.AggregateReconciler[*corev1.ConfigMap] { + return &reconcilers.AggregateReconciler[*corev1.ConfigMap]{ Request: request, DesiredResource: func(ctx context.Context, resource *corev1.ConfigMap) (*corev1.ConfigMap, error) { @@ -828,7 +821,7 @@ func TestAggregateReconciler(t *testing.T) { }, }, "ignore other resources": { - Request: controllerruntime.Request{ + Request: reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: testNamespace, Name: "not-it"}, }, Metadata: map[string]interface{}{ @@ -894,7 +887,7 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.DesiredResource = func(ctx context.Context, resource client.Object) (client.Object, error) { + r.DesiredResource = func(ctx context.Context, resource *corev1.ConfigMap) (*corev1.ConfigMap, error) { return nil, nil } return r @@ -1032,7 +1025,7 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.DesiredResource = func(ctx context.Context, resource client.Object) (client.Object, error) { + r.DesiredResource = func(ctx context.Context, resource *corev1.ConfigMap) (*corev1.ConfigMap, error) { return nil, nil } return r @@ -1056,23 +1049,23 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource client.Object) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: time.Hour}, nil + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ + SyncWithResult: func(ctx context.Context, resource *corev1.ConfigMap) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: time.Hour}, nil }, } return r }, }, - ExpectedResult: ctrl.Result{RequeueAfter: time.Hour}, + ExpectedResult: reconcilers.Result{RequeueAfter: time.Hour}, }, "reconcile error": { Request: request, Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource client.Object) error { + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ + Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return fmt.Errorf("test error") }, } @@ -1086,14 +1079,14 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource client.Object) error { + r.Reconciler = reconcilers.Sequence[*corev1.ConfigMap]{ + &reconcilers.SyncReconciler[*corev1.ConfigMap]{ + Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return reconcilers.HaltSubReconcilers }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource client.Object) error { + &reconcilers.SyncReconciler[*corev1.ConfigMap]{ + Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return fmt.Errorf("test error") }, }, @@ -1119,7 +1112,7 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { var key reconcilers.StashKey = "foo" // StashValue will panic if the context is not setup correctly @@ -1140,7 +1133,7 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { if config := reconcilers.RetrieveConfigOrDie(ctx); config != c { t.Errorf("expected config in context, found %#v", config) @@ -1164,7 +1157,7 @@ func TestAggregateReconciler(t *testing.T) { Metadata: map[string]interface{}{ "Reconciler": func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { if resourceType, ok := reconcilers.RetrieveOriginalResourceType(ctx).(*corev1.ConfigMap); !ok { t.Errorf("expected original resource type not in context, found %#v", resourceType) @@ -1192,7 +1185,7 @@ func TestAggregateReconciler(t *testing.T) { tc.Metadata["Reconciler"] = func(t *testing.T, c reconcilers.Config) reconcile.Reconciler { r := defaultAggregateReconciler(c) - r.Reconciler = &reconcilers.SyncReconciler{ + r.Reconciler = &reconcilers.SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { if v := ctx.Value(key); v != value { t.Errorf("expected %s to be in context", key) @@ -1239,12 +1232,12 @@ func TestSyncReconciler(t *testing.T) { ) }) - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "sync success": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return nil }, @@ -1253,10 +1246,10 @@ func TestSyncReconciler(t *testing.T) { }, }, "sync error": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return fmt.Errorf("syncreconciler error") }, @@ -1266,40 +1259,27 @@ func TestSyncReconciler(t *testing.T) { ShouldErr: true, }, "missing sync method": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: nil, } }, }, ShouldPanic: true, }, - "invalid sync signature": { - Resource: resource, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource string) error { - return nil - }, - } - }, - }, - ShouldPanic: true, - }, "should not finalize non-deleted resources": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 2 * time.Hour}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 2 * time.Hour}, nil }, - Finalize: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { + FinalizeWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { t.Errorf("reconciler should not call finalize for non-deleted resources") - return ctrl.Result{RequeueAfter: 3 * time.Hour}, nil + return reconcilers.Result{RequeueAfter: 3 * time.Hour}, nil }, } }, @@ -1310,16 +1290,17 @@ func TestSyncReconciler(t *testing.T) { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { t.Errorf("reconciler should not call sync for deleted resources") - return ctrl.Result{RequeueAfter: 2 * time.Hour}, nil + return reconcilers.Result{RequeueAfter: 2 * time.Hour}, nil }, - Finalize: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 3 * time.Hour}, nil + FinalizeWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 3 * time.Hour}, nil }, } }, @@ -1330,16 +1311,17 @@ func TestSyncReconciler(t *testing.T) { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ SyncDuringFinalization: true, - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 2 * time.Hour}, nil + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 2 * time.Hour}, nil }, - Finalize: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 3 * time.Hour}, nil + FinalizeWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 3 * time.Hour}, nil }, } }, @@ -1350,16 +1332,17 @@ func TestSyncReconciler(t *testing.T) { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ SyncDuringFinalization: true, - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 3 * time.Hour}, nil + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 3 * time.Hour}, nil }, - Finalize: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 2 * time.Hour}, nil + FinalizeWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 2 * time.Hour}, nil }, } }, @@ -1370,10 +1353,11 @@ func TestSyncReconciler(t *testing.T) { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return nil }, @@ -1385,10 +1369,11 @@ func TestSyncReconciler(t *testing.T) { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return nil }, @@ -1401,14 +1386,14 @@ func TestSyncReconciler(t *testing.T) { ShouldErr: true, }, "context can be augmented in Prepare and accessed in Cleanup": { - Resource: resource, - Prepare: func(t *testing.T, ctx context.Context, tc *rtesting.SubReconcilerTestCase) (context.Context, error) { + Resource: resource.DieReleasePtr(), + Prepare: func(t *testing.T, ctx context.Context, tc *rtesting.SubReconcilerTestCase[*resources.TestResource]) (context.Context, error) { key := "test-key" value := "test-value" ctx = context.WithValue(ctx, key, value) - tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if v := ctx.Value(key); v != value { t.Errorf("expected %s to be in context", key) @@ -1417,7 +1402,7 @@ func TestSyncReconciler(t *testing.T) { }, } } - tc.CleanUp = func(t *testing.T, ctx context.Context, tc *rtesting.SubReconcilerTestCase) error { + tc.CleanUp = func(t *testing.T, ctx context.Context, tc *rtesting.SubReconcilerTestCase[*resources.TestResource]) error { if v := ctx.Value(key); v != value { t.Errorf("expected %s to be in context", key) } @@ -1429,8 +1414,8 @@ func TestSyncReconciler(t *testing.T) { }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) }) } @@ -1473,11 +1458,8 @@ func TestChildReconciler(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { }) - defaultChildReconciler := func(c reconcilers.Config) *reconcilers.ChildReconciler { - return &reconcilers.ChildReconciler{ - ChildType: &corev1.ConfigMap{}, - ChildListType: &corev1.ConfigMapList{}, - + defaultChildReconciler := func(c reconcilers.Config) *reconcilers.ChildReconciler[*resources.TestResource, *corev1.ConfigMap, *corev1.ConfigMapList] { + return &reconcilers.ChildReconciler[*resources.TestResource, *corev1.ConfigMap, *corev1.ConfigMapList]{ DesiredChild: func(ctx context.Context, parent *resources.TestResource) (*corev1.ConfigMap, error) { if len(parent.Spec.Fields) == 0 { return nil, nil @@ -1513,11 +1495,11 @@ func TestChildReconciler(t *testing.T) { } } - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "preserve no child": { - Resource: resourceReady, + Resource: resourceReady.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1529,12 +1511,13 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1546,7 +1529,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1554,7 +1538,7 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.ListOptions = func(ctx context.Context, parent *resources.TestResource) []client.ListOption { return []client.ListOption{ @@ -1569,9 +1553,10 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1585,7 +1570,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), ExpectCreates: []client.Object{ configMapCreate, }, @@ -1594,13 +1580,14 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1623,7 +1610,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), ExpectCreates: []client.Object{ configMapCreate. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1649,12 +1637,13 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1670,7 +1659,8 @@ func TestChildReconciler(t *testing.T) { StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") d.AddField("new", "field") - }), + }). + DieReleasePtr(), ExpectUpdates: []client.Object{ configMapGiven. AddData("new", "field"), @@ -1687,7 +1677,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1695,11 +1686,11 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1721,7 +1712,8 @@ func TestChildReconciler(t *testing.T) { StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") d.AddField("new", "field") - }), + }). + DieReleasePtr(), ExpectUpdates: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1738,7 +1730,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1746,11 +1739,11 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1775,7 +1768,8 @@ func TestChildReconciler(t *testing.T) { StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") d.AddField("new", "field") - }), + }). + DieReleasePtr(), ExpectUpdates: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1795,12 +1789,12 @@ func TestChildReconciler(t *testing.T) { }, }, "delete child": { - Resource: resourceReady, + Resource: resourceReady.DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1816,7 +1810,8 @@ func TestChildReconciler(t *testing.T) { Resource: resourceReady. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Finalizers(testFinalizer, "some.other.finalizer") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1824,11 +1819,11 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1841,12 +1836,12 @@ func TestChildReconciler(t *testing.T) { }, }, "ignore extraneous children": { - Resource: resourceReady, + Resource: resourceReady.DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return false @@ -1859,7 +1854,8 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1871,7 +1867,7 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -1881,7 +1877,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Deleted", `Deleted ConfigMap %q`, "extra-child-1"), @@ -1903,7 +1900,8 @@ func TestChildReconciler(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1911,11 +1909,11 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1932,13 +1930,14 @@ func TestChildReconciler(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1951,7 +1950,8 @@ func TestChildReconciler(t *testing.T) { d.DeletionTimestamp(&now) d.Finalizers() d.ResourceVersion("1000") - }), + }). + DieReleasePtr(), ExpectPatches: []rtesting.PatchRef{ { Group: "testing.reconciler.runtime", @@ -1968,7 +1968,8 @@ func TestChildReconciler(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(&now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -1977,11 +1978,11 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -1990,14 +1991,15 @@ func TestChildReconciler(t *testing.T) { Resource: resourceReady. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), WithReactors: []rtesting.ReactionFunc{ rtesting.InduceFailure("create", "ConfigMap", rtesting.InduceFailureOpts{ Error: apierrs.NewAlreadyExists(schema.GroupResource{}, testName), }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2010,7 +2012,8 @@ func TestChildReconciler(t *testing.T) { diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionFalse). Reason("NameConflict").Message(`"test-resource" already exists`), ) - }), + }). + DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeWarning, "CreationFailed", "Failed to create ConfigMap %q: %q already exists", testName, testName), @@ -2023,7 +2026,8 @@ func TestChildReconciler(t *testing.T) { Resource: resourceReady. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), APIGivenObjects: []client.Object{ configMapGiven, }, @@ -2033,7 +2037,7 @@ func TestChildReconciler(t *testing.T) { }), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2054,13 +2058,14 @@ func TestChildReconciler(t *testing.T) { StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") d.AddField("immutable", "field") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. AddData("immutable", "field"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.HarmonizeImmutableFields = func(current, desired *corev1.ConfigMap) { desired.Data["immutable"] = current.Data["immutable"] @@ -2070,16 +2075,17 @@ func TestChildReconciler(t *testing.T) { }, }, "status only reconcile": { - Resource: resource, + Resource: resource.DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, ExpectResource: resourceReady. StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.DesiredChild = func(ctx context.Context, parent *resources.TestResource) (*corev1.ConfigMap, error) { return nil, reconcilers.OnlyReconcileChildStatus @@ -2092,9 +2098,10 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Sanitize = func(child *corev1.ConfigMap) interface{} { return child.Name @@ -2112,7 +2119,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), ExpectCreates: []client.Object{ configMapCreate, }, @@ -2121,9 +2129,10 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Sanitize = func(child *corev1.ConfigMap) interface{} { child.Data["ignore"] = "me" @@ -2142,18 +2151,19 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), ExpectCreates: []client.Object{ configMapCreate, }, }, "error listing children": { - Resource: resourceReady, + Resource: resourceReady.DieReleasePtr(), WithReactors: []rtesting.ReactionFunc{ rtesting.InduceFailure("list", "ConfigMapList"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2163,12 +2173,13 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), WithReactors: []rtesting.ReactionFunc{ rtesting.InduceFailure("create", "ConfigMap"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2185,13 +2196,14 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -2218,13 +2230,14 @@ func TestChildReconciler(t *testing.T) { Resource: resourceReady. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.Finalizer = testFinalizer r.SkipOwnerReference = true - r.OurChild = func(parent, child client.Object) bool { return true } + r.OurChild = func(parent *resources.TestResource, child *corev1.ConfigMap) bool { return true } return r }, }, @@ -2255,7 +2268,8 @@ func TestChildReconciler(t *testing.T) { }). StatusDie(func(d *dies.TestResourceStatusDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, @@ -2263,7 +2277,7 @@ func TestChildReconciler(t *testing.T) { rtesting.InduceFailure("update", "ConfigMap"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2278,7 +2292,7 @@ func TestChildReconciler(t *testing.T) { ShouldErr: true, }, "error deleting child": { - Resource: resourceReady, + Resource: resourceReady.DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven, }, @@ -2286,7 +2300,7 @@ func TestChildReconciler(t *testing.T) { rtesting.InduceFailure("delete", "ConfigMap"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2303,7 +2317,8 @@ func TestChildReconciler(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.AddField("foo", "bar") - }), + }). + DieReleasePtr(), GivenObjects: []client.Object{ configMapGiven. MetadataDie(func(d *diemetav1.ObjectMetaDie) { @@ -2318,7 +2333,7 @@ func TestChildReconciler(t *testing.T) { rtesting.InduceFailure("delete", "ConfigMap"), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { return defaultChildReconciler(c) }, }, @@ -2332,9 +2347,9 @@ func TestChildReconciler(t *testing.T) { ShouldErr: true, }, "error creating desired child": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { r := defaultChildReconciler(c) r.DesiredChild = func(ctx context.Context, parent *resources.TestResource) (*corev1.ConfigMap, error) { return nil, fmt.Errorf("test error") @@ -2346,8 +2361,8 @@ func TestChildReconciler(t *testing.T) { }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) }) } @@ -2369,13 +2384,13 @@ func TestSequence(t *testing.T) { ) }) - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "sub reconciler erred": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return fmt.Errorf("reconciler error") }, @@ -2386,213 +2401,213 @@ func TestSequence(t *testing.T) { ShouldErr: true, }, "preserves result, Requeue": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, } }, }, - ExpectedResult: ctrl.Result{Requeue: true}, + ExpectedResult: reconcilers.Result{Requeue: true}, }, "preserves result, RequeueAfter": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "ignores result on err": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, fmt.Errorf("test error") + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, fmt.Errorf("test error") }, }, } }, }, - ExpectedResult: ctrl.Result{}, + ExpectedResult: reconcilers.Result{}, ShouldErr: true, }, "Requeue + empty => Requeue": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{Requeue: true}, + ExpectedResult: reconcilers.Result{Requeue: true}, }, "empty + Requeue => Requeue": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{Requeue: true}, + ExpectedResult: reconcilers.Result{Requeue: true}, }, "RequeueAfter + empty => RequeueAfter": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "empty + RequeueAfter => RequeueAfter": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "RequeueAfter + Requeue => RequeueAfter": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "Requeue + RequeueAfter => RequeueAfter": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "RequeueAfter(1m) + RequeueAfter(2m) => RequeueAfter(1m)": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 2 * time.Minute}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, "RequeueAfter(2m) + RequeueAfter(1m) => RequeueAfter(1m)": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 2 * time.Minute}, nil }, }, - &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) (ctrl.Result, error) { - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + &reconcilers.SyncReconciler[*resources.TestResource]{ + SyncWithResult: func(ctx context.Context, resource *resources.TestResource) (reconcilers.Result, error) { + return reconcilers.Result{RequeueAfter: 1 * time.Minute}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{RequeueAfter: 1 * time.Minute}, + ExpectedResult: reconcilers.Result{RequeueAfter: 1 * time.Minute}, }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) }) } @@ -2615,7 +2630,7 @@ func TestCastResource(t *testing.T) { ) }) - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "sync success": { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { @@ -2624,12 +2639,12 @@ func TestCastResource(t *testing.T) { d.ContainerDie("test-container", func(d *diecorev1.ContainerDie) {}) }) }) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *appsv1.Deployment]{ + Reconciler: &reconcilers.SyncReconciler[*appsv1.Deployment]{ Sync: func(ctx context.Context, resource *appsv1.Deployment) error { reconcilers.RetrieveConfigOrDie(ctx). Recorder.Event(resource, corev1.EventTypeNormal, "Test", @@ -2645,7 +2660,7 @@ func TestCastResource(t *testing.T) { }, }, "cast mutation": { - Resource: resource, + Resource: resource.DieReleasePtr(), ExpectResource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { @@ -2653,12 +2668,12 @@ func TestCastResource(t *testing.T) { d.Name("mutation") }) }) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *appsv1.Deployment]{ + Reconciler: &reconcilers.SyncReconciler[*appsv1.Deployment]{ Sync: func(ctx context.Context, resource *appsv1.Deployment) error { // mutation that exists on the original resource and will be reflected resource.Spec.Template.Name = "mutation" @@ -2672,28 +2687,26 @@ func TestCastResource(t *testing.T) { }, }, "return subreconciler result": { - Resource: resource, - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *appsv1.Deployment) (ctrl.Result, error) { - return ctrl.Result{Requeue: true}, nil + Resource: resource.DieReleasePtr(), + Metadata: map[string]interface{}{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *appsv1.Deployment]{ + Reconciler: &reconcilers.SyncReconciler[*appsv1.Deployment]{ + SyncWithResult: func(ctx context.Context, resource *appsv1.Deployment) (reconcilers.Result, error) { + return reconcilers.Result{Requeue: true}, nil }, }, } }, }, - ExpectedResult: ctrl.Result{Requeue: true}, + ExpectedResult: reconcilers.Result{Requeue: true}, }, "return subreconciler err": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *appsv1.Deployment]{ + Reconciler: &reconcilers.SyncReconciler[*appsv1.Deployment]{ Sync: func(ctx context.Context, resource *appsv1.Deployment) error { return fmt.Errorf("subreconciler error") }, @@ -2703,62 +2716,16 @@ func TestCastResource(t *testing.T) { }, ShouldErr: true, }, - "subreconcilers must be compatible with cast value, not reconciled resource": { - Resource: resource. - SpecDie(func(d *dies.TestResourceSpecDie) { - d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { - d.SpecDie(func(d *diecorev1.PodSpecDie) { - d.ContainerDie("test-container", func(d *diecorev1.ContainerDie) {}) - }) - }) - }), - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) error { - return nil - }, - }, - } - }, - }, - ShouldPanic: true, - }, - "error for cast to different type than expected by sub reconciler": { - Resource: resource. - SpecDie(func(d *dies.TestResourceSpecDie) { - d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { - d.SpecDie(func(d *diecorev1.PodSpecDie) { - d.ContainerDie("test-container", func(d *diecorev1.ContainerDie) {}) - }) - }) - }), - Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &appsv1.Deployment{}, - Reconciler: &reconcilers.SyncReconciler{ - Sync: func(ctx context.Context, resource *resources.TestResource) error { - return nil - }, - }, - } - }, - }, - ShouldPanic: true, - }, "marshal error": { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.ErrOnMarshal(true) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &resources.TestResource{}, - Reconciler: &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *resources.TestResource]{ + Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c.Recorder.Event(resource, corev1.EventTypeNormal, "Test", resource.Name) return nil @@ -2773,12 +2740,12 @@ func TestCastResource(t *testing.T) { Resource: resource. SpecDie(func(d *dies.TestResourceSpecDie) { d.ErrOnUnmarshal(true) - }), + }). + DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.CastResource{ - Type: &resources.TestResource{}, - Reconciler: &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.CastResource[*resources.TestResource, *resources.TestResource]{ + Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c.Recorder.Event(resource, corev1.EventTypeNormal, "Test", resource.Name) return nil @@ -2791,8 +2758,8 @@ func TestCastResource(t *testing.T) { }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) }) } @@ -2809,20 +2776,20 @@ func TestWithConfig(t *testing.T) { d.Name(testName) }) - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "with config": { - Resource: resource, + Resource: resource.DieReleasePtr(), Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, oc reconcilers.Config) reconcilers.SubReconciler { + "SubReconciler": func(t *testing.T, oc reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { c := reconcilers.Config{ Tracker: tracker.New(0), } - return &reconcilers.WithConfig{ + return &reconcilers.WithConfig[*resources.TestResource]{ Config: func(ctx context.Context, _ reconcilers.Config) (reconcilers.Config, error) { return c, nil }, - Reconciler: &reconcilers.SyncReconciler{ + Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, parent *resources.TestResource) error { rc := reconcilers.RetrieveConfigOrDie(ctx) roc := reconcilers.RetrieveOriginalConfigOrDie(ctx) @@ -2848,8 +2815,8 @@ func TestWithConfig(t *testing.T) { }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { - return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c) + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return rtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c) }) } @@ -2869,23 +2836,25 @@ func TestWithFinalizer(t *testing.T) { d.Name(testName) }) - rts := rtesting.SubReconcilerTests{ + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ "in sync": { Resource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Sync", ""), }, }, "add finalizer": { - Resource: resource, + Resource: resource.DieReleasePtr(), ExpectResource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Finalizers(testFinalizer) d.ResourceVersion("1000") - }), + }). + DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "FinalizerPatched", `Patched finalizer %q`, testFinalizer), @@ -2903,7 +2872,7 @@ func TestWithFinalizer(t *testing.T) { }, }, "error adding finalizer": { - Resource: resource, + Resource: resource.DieReleasePtr(), WithReactors: []rtesting.ReactionFunc{ rtesting.InduceFailure("patch", "TestResource"), }, @@ -2928,12 +2897,14 @@ func TestWithFinalizer(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), ExpectResource: resource. MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(now) d.ResourceVersion("1000") - }), + }). + DieReleasePtr(), ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Finalize", ""), rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "FinalizerPatched", @@ -2955,7 +2926,8 @@ func TestWithFinalizer(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), WithReactors: []rtesting.ReactionFunc{ rtesting.InduceFailure("patch", "TestResource"), }, @@ -2981,7 +2953,8 @@ func TestWithFinalizer(t *testing.T) { MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.DeletionTimestamp(now) d.Finalizers(testFinalizer) - }), + }). + DieReleasePtr(), ShouldErr: true, ExpectEvents: []rtesting.Event{ rtesting.NewEvent(resource, scheme, corev1.EventTypeNormal, "Finalize", ""), @@ -2992,7 +2965,7 @@ func TestWithFinalizer(t *testing.T) { }, } - rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler { + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { var syncErr, finalizeErr error if err, ok := rtc.Metadata["SyncError"]; ok { syncErr = err.(error) @@ -3001,9 +2974,9 @@ func TestWithFinalizer(t *testing.T) { finalizeErr = err.(error) } - return &reconcilers.WithFinalizer{ + return &reconcilers.WithFinalizer[*resources.TestResource]{ Finalizer: testFinalizer, - Reconciler: &reconcilers.SyncReconciler{ + Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c.Recorder.Event(resource, corev1.EventTypeNormal, "Sync", "") return syncErr diff --git a/reconcilers/reconcilers_validate_test.go b/reconcilers/reconcilers_validate_test.go index b30ca43..f1668dc 100644 --- a/reconcilers/reconcilers_validate_test.go +++ b/reconcilers/reconcilers_validate_test.go @@ -15,61 +15,98 @@ import ( "github.com/vmware-labs/reconciler-runtime/tracker" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -func TestResourceReconciler_validate(t *testing.T) { +func TestResourceReconciler_validate_TestResource(t *testing.T) { tests := []struct { name string - reconciler *ResourceReconciler + reconciler *ResourceReconciler[*resources.TestResource] shouldErr string expectedLogs []string }{ - { - name: "empty", - reconciler: &ResourceReconciler{}, - shouldErr: `ResourceReconciler "" must define Type`, - }, { name: "valid", - reconciler: &ResourceReconciler{ - Type: &resources.TestResource{}, - Reconciler: Sequence{}, + reconciler: &ResourceReconciler[*resources.TestResource]{ + Reconciler: Sequence[*resources.TestResource]{}, }, }, { - name: "missing type", - reconciler: &ResourceReconciler{ - Name: "missing type", - Reconciler: Sequence{}, + name: "with type", + reconciler: &ResourceReconciler[*resources.TestResource]{ + Name: "with type", + Type: &resources.TestResource{}, + Reconciler: Sequence[*resources.TestResource]{}, }, - shouldErr: `ResourceReconciler "missing type" must define Type`, }, { name: "missing reconciler", - reconciler: &ResourceReconciler{ + reconciler: &ResourceReconciler[*resources.TestResource]{ Name: "missing reconciler", - Type: &resources.TestResource{}, }, shouldErr: `ResourceReconciler "missing reconciler" must define Reconciler`, }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + sink := &bufferedSink{} + ctx := logr.NewContext(context.TODO(), logr.New(sink)) + err := c.reconciler.validate(ctx) + if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { + t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr) + } + if diff := cmp.Diff(c.expectedLogs, sink.Lines); diff != "" { + t.Errorf("%s: unexpected logs (-expected, +actual): %s", c.name, diff) + } + }) + } +} + +func TestResourceReconciler_validate_TestResourceNoStatus(t *testing.T) { + tests := []struct { + name string + reconciler *ResourceReconciler[*resources.TestResourceNoStatus] + shouldErr string + expectedLogs []string + }{ { name: "type has no status", - reconciler: &ResourceReconciler{ - Type: &resources.TestResourceNoStatus{}, - Reconciler: Sequence{}, + reconciler: &ResourceReconciler[*resources.TestResourceNoStatus]{ + Reconciler: Sequence[*resources.TestResourceNoStatus]{}, }, expectedLogs: []string{ "resource missing status field, operations related to status will be skipped", }, }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + sink := &bufferedSink{} + ctx := logr.NewContext(context.TODO(), logr.New(sink)) + err := c.reconciler.validate(ctx) + if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { + t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr) + } + if diff := cmp.Diff(c.expectedLogs, sink.Lines); diff != "" { + t.Errorf("%s: unexpected logs (-expected, +actual): %s", c.name, diff) + } + }) + } +} + +func TestResourceReconciler_validate_TestResourceEmptyStatus(t *testing.T) { + tests := []struct { + name string + reconciler *ResourceReconciler[*resources.TestResourceEmptyStatus] + shouldErr string + expectedLogs []string + }{ { name: "type has empty status", - reconciler: &ResourceReconciler{ - Type: &resources.TestResourceEmptyStatus{}, - Reconciler: Sequence{}, + reconciler: &ResourceReconciler[*resources.TestResourceEmptyStatus]{ + Reconciler: Sequence[*resources.TestResourceEmptyStatus]{}, }, expectedLogs: []string{ "resource status missing ObservedGeneration field of type int64, generation will not be managed", @@ -77,11 +114,34 @@ func TestResourceReconciler_validate(t *testing.T) { "resource status is missing field Conditions of type []metav1.Condition, condition timestamps will not be managed", }, }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + sink := &bufferedSink{} + ctx := logr.NewContext(context.TODO(), logr.New(sink)) + err := c.reconciler.validate(ctx) + if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) { + t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr) + } + if diff := cmp.Diff(c.expectedLogs, sink.Lines); diff != "" { + t.Errorf("%s: unexpected logs (-expected, +actual): %s", c.name, diff) + } + }) + } +} + +func TestResourceReconciler_validate_TestResourceNilableStatus(t *testing.T) { + tests := []struct { + name string + reconciler *ResourceReconciler[*resources.TestResourceNilableStatus] + shouldErr string + expectedLogs []string + }{ { name: "type has nilable status", - reconciler: &ResourceReconciler{ - Type: &resources.TestResourceNilableStatus{}, - Reconciler: Sequence{}, + reconciler: &ResourceReconciler[*resources.TestResourceNilableStatus]{ + Reconciler: Sequence[*resources.TestResourceNilableStatus]{}, }, expectedLogs: []string{ "resource status is nilable, status is typically a struct", @@ -105,7 +165,7 @@ func TestResourceReconciler_validate(t *testing.T) { } func TestAggregateReconciler_validate(t *testing.T) { - req := reconcile.Request{ + req := Request{ NamespacedName: types.NamespacedName{ Namespace: "my-namespace", Name: "my-name", @@ -114,49 +174,48 @@ func TestAggregateReconciler_validate(t *testing.T) { tests := []struct { name string - reconciler *AggregateReconciler + reconciler *AggregateReconciler[*resources.TestResource] shouldErr string expectedLogs []string }{ { name: "empty", - reconciler: &AggregateReconciler{}, - shouldErr: `AggregateReconciler "" must define Type`, + reconciler: &AggregateReconciler[*resources.TestResource]{}, + shouldErr: `AggregateReconciler "" must define Request`, }, { name: "valid", - reconciler: &AggregateReconciler{ + reconciler: &AggregateReconciler[*resources.TestResource]{ Type: &resources.TestResource{}, Request: req, - Reconciler: Sequence{}, + Reconciler: Sequence[*resources.TestResource]{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, }, { name: "Type missing", - reconciler: &AggregateReconciler{ + reconciler: &AggregateReconciler[*resources.TestResource]{ Name: "Type missing", // Type: &resources.TestResource{}, Request: req, - Reconciler: Sequence{}, + Reconciler: Sequence[*resources.TestResource]{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, - shouldErr: `AggregateReconciler "Type missing" must define Type`, }, { name: "Request missing", - reconciler: &AggregateReconciler{ + reconciler: &AggregateReconciler[*resources.TestResource]{ Name: "Request missing", Type: &resources.TestResource{}, // Request: req, - Reconciler: Sequence{}, + Reconciler: Sequence[*resources.TestResource]{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, shouldErr: `AggregateReconciler "Request missing" must define Request`, }, { name: "Reconciler missing", - reconciler: &AggregateReconciler{ + reconciler: &AggregateReconciler[*resources.TestResource]{ Name: "Reconciler missing", Type: &resources.TestResource{}, Request: req, @@ -167,98 +226,16 @@ func TestAggregateReconciler_validate(t *testing.T) { }, { name: "DesiredResource", - reconciler: &AggregateReconciler{ + reconciler: &AggregateReconciler[*resources.TestResource]{ Type: &resources.TestResource{}, Request: req, - Reconciler: Sequence{}, + Reconciler: Sequence[*resources.TestResource]{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, DesiredResource: func(ctx context.Context, resource *resources.TestResource) (*resources.TestResource, error) { return nil, nil }, }, }, - { - name: "DesiredResource num in", - reconciler: &AggregateReconciler{ - Name: "DesiredResource num in", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func() (*resources.TestResource, error) { - return nil, nil - }, - }, - shouldErr: `AggregateReconciler "DesiredResource num in" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func() (*resources.TestResource, error)`, - }, - { - name: "DesiredResource in 0", - reconciler: &AggregateReconciler{ - Name: "DesiredResource in 0", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func(err error, resource *resources.TestResource) (*resources.TestResource, error) { - return nil, nil - }, - }, - shouldErr: `AggregateReconciler "DesiredResource in 0" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func(error, *resources.TestResource) (*resources.TestResource, error)`, - }, - { - name: "DesiredResource in 1", - reconciler: &AggregateReconciler{ - Name: "DesiredResource in 1", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func(ctx context.Context, resource *corev1.Pod) (*resources.TestResource, error) { - return nil, nil - }, - }, - shouldErr: `AggregateReconciler "DesiredResource in 1" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func(context.Context, *v1.Pod) (*resources.TestResource, error)`, - }, - { - name: "DesiredResource num out", - reconciler: &AggregateReconciler{ - Name: "DesiredResource num out", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func(ctx context.Context, resource *resources.TestResource) {}, - }, - shouldErr: `AggregateReconciler "DesiredResource num out" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func(context.Context, *resources.TestResource)`, - }, - { - name: "DesiredResource out 0", - reconciler: &AggregateReconciler{ - Name: "DesiredResource out 0", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func(ctx context.Context, resource *resources.TestResource) (*corev1.Pod, error) { - return nil, nil - }, - }, - shouldErr: `AggregateReconciler "DesiredResource out 0" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func(context.Context, *resources.TestResource) (*v1.Pod, error)`, - }, - { - name: "DesiredResource out 1", - reconciler: &AggregateReconciler{ - Name: "DesiredResource out 1", - Type: &resources.TestResource{}, - Request: req, - Reconciler: Sequence{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - DesiredResource: func(ctx context.Context, resource *resources.TestResource) (*resources.TestResource, string) { - return nil, "" - }, - }, - shouldErr: `AggregateReconciler "DesiredResource out 1" must implement DesiredResource: nil | func(context.Context, *resources.TestResource) (*resources.TestResource, error), found: func(context.Context, *resources.TestResource) (*resources.TestResource, string)`, - }, } for _, c := range tests { @@ -280,91 +257,59 @@ func TestSyncReconciler_validate(t *testing.T) { tests := []struct { name string resource client.Object - reconciler *SyncReconciler + reconciler *SyncReconciler[*corev1.ConfigMap] shouldErr string }{ { name: "empty", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{}, - shouldErr: `SyncReconciler "" must implement Sync`, + reconciler: &SyncReconciler[*corev1.ConfigMap]{}, + shouldErr: `SyncReconciler "" must implement Sync or SyncWithResult`, }, { name: "valid", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ + reconciler: &SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, }, }, { - name: "valid Sync with result", + name: "valid SyncWithResult", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Sync: func(ctx context.Context, resource *corev1.ConfigMap) (ctrl.Result, error) { - return ctrl.Result{}, nil + reconciler: &SyncReconciler[*corev1.ConfigMap]{ + SyncWithResult: func(ctx context.Context, resource *corev1.ConfigMap) (Result, error) { + return Result{}, nil }, }, }, { - name: "Sync num in", + name: "valid", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Sync num in", - Sync: func() error { + reconciler: &SyncReconciler[*corev1.ConfigMap]{ + Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, }, - shouldErr: `SyncReconciler "Sync num in" must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func() error`, }, { - name: "Sync in 1", + name: "invalid Sync and SyncWithResult", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Sync in 1", - Sync: func(ctx context.Context, resource *corev1.Secret) error { + reconciler: &SyncReconciler[*corev1.ConfigMap]{ + Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, - }, - shouldErr: `SyncReconciler "Sync in 1" must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.Secret) error`, - }, - { - name: "Sync num out", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Sync num out", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) { + SyncWithResult: func(ctx context.Context, resource *corev1.ConfigMap) (Result, error) { + return Result{}, nil }, }, - shouldErr: `SyncReconciler "Sync num out" must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap)`, - }, - { - name: "Sync out 1", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Sync out 1", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) string { - return "" - }, - }, - shouldErr: `SyncReconciler "Sync out 1" must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap) string`, - }, - { - name: "Sync result out 1", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Sync result out 1", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) (ctrl.Result, string) { - return ctrl.Result{}, "" - }, - }, - shouldErr: `SyncReconciler "Sync result out 1" must implement Sync: func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap) (reconcile.Result, string)`, + shouldErr: `SyncReconciler "" may not implement both Sync and SyncWithResult`, }, { name: "valid Finalize", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ + reconciler: &SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, @@ -376,83 +321,30 @@ func TestSyncReconciler_validate(t *testing.T) { { name: "valid Finalize with result", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ + reconciler: &SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, - Finalize: func(ctx context.Context, resource *corev1.ConfigMap) (ctrl.Result, error) { - return ctrl.Result{}, nil - }, - }, - }, - { - name: "Finalize num in", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Finalize num in", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { - return nil - }, - Finalize: func() error { - return nil - }, - }, - shouldErr: `SyncReconciler "Finalize num in" must implement Finalize: nil | func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func() error`, - }, - { - name: "Finalize in 1", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Finalize in 1", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { - return nil - }, - Finalize: func(ctx context.Context, resource *corev1.Secret) error { - return nil + FinalizeWithResult: func(ctx context.Context, resource *corev1.ConfigMap) (Result, error) { + return Result{}, nil }, }, - shouldErr: `SyncReconciler "Finalize in 1" must implement Finalize: nil | func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.Secret) error`, }, { - name: "Finalize num out", + name: "invalid Finalize and FinalizeWithResult", resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Finalize num out", + reconciler: &SyncReconciler[*corev1.ConfigMap]{ Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, - Finalize: func(ctx context.Context, resource *corev1.ConfigMap) { - }, - }, - shouldErr: `SyncReconciler "Finalize num out" must implement Finalize: nil | func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap)`, - }, - { - name: "Finalize out 1", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Finalize out 1", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { - return nil - }, - Finalize: func(ctx context.Context, resource *corev1.ConfigMap) string { - return "" - }, - }, - shouldErr: `SyncReconciler "Finalize out 1" must implement Finalize: nil | func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap) string`, - }, - { - name: "Finalize result out 1", - resource: &corev1.ConfigMap{}, - reconciler: &SyncReconciler{ - Name: "Finalize result out 1", - Sync: func(ctx context.Context, resource *corev1.ConfigMap) error { + Finalize: func(ctx context.Context, resource *corev1.ConfigMap) error { return nil }, - Finalize: func(ctx context.Context, resource *corev1.ConfigMap) (ctrl.Result, string) { - return ctrl.Result{}, "" + FinalizeWithResult: func(ctx context.Context, resource *corev1.ConfigMap) (Result, error) { + return Result{}, nil }, }, - shouldErr: `SyncReconciler "Finalize result out 1" must implement Finalize: nil | func(context.Context, *v1.ConfigMap) error | func(context.Context, *v1.ConfigMap) (ctrl.Result, error), found: func(context.Context, *v1.ConfigMap) (reconcile.Result, string)`, + shouldErr: `SyncReconciler "" may not implement both Finalize and FinalizeWithResult`, }, } @@ -470,20 +362,20 @@ func TestSyncReconciler_validate(t *testing.T) { func TestChildReconciler_validate(t *testing.T) { tests := []struct { name string - parent client.Object - reconciler *ChildReconciler + parent *corev1.ConfigMap + reconciler *ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList] shouldErr string }{ { name: "empty", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{}, - shouldErr: `ChildReconciler "" must define ChildType`, + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{}, + shouldErr: `ChildReconciler "" must implement DesiredChild`, }, { name: "valid", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + 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 }, @@ -494,7 +386,7 @@ func TestChildReconciler_validate(t *testing.T) { { name: "ChildType missing", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "ChildType missing", // ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -502,12 +394,11 @@ func TestChildReconciler_validate(t *testing.T) { ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, - shouldErr: `ChildReconciler "ChildType missing" must define ChildType`, }, { name: "ChildListType missing", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "ChildListType missing", ChildType: &corev1.Pod{}, // ChildListType: &corev1.PodList{}, @@ -515,12 +406,11 @@ func TestChildReconciler_validate(t *testing.T) { ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, }, - shouldErr: `ChildReconciler "ChildListType missing" must define ChildListType`, }, { name: "DesiredChild missing", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "DesiredChild missing", ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -530,88 +420,10 @@ func TestChildReconciler_validate(t *testing.T) { }, shouldErr: `ChildReconciler "DesiredChild missing" must implement DesiredChild`, }, - { - name: "DesiredChild num in", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild num in", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func() (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild num in" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func() (*v1.Pod, error)`, - }, - { - name: "DesiredChild in 0", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild in 0", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx string, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild in 0" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func(string, *v1.ConfigMap) (*v1.Pod, error)`, - }, - { - name: "DesiredChild in 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild in 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.Secret) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild in 1" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func(context.Context, *v1.Secret) (*v1.Pod, error)`, - }, - { - name: "DesiredChild num out", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild num out", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) {}, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild num out" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func(context.Context, *v1.ConfigMap)`, - }, - { - name: "DesiredChild out 0", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild out 0", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Secret, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild out 0" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func(context.Context, *v1.ConfigMap) (*v1.Secret, error)`, - }, - { - name: "DesiredChild out 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "DesiredChild out 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, string) { return nil, "" }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "DesiredChild out 1" must implement DesiredChild: func(context.Context, *v1.ConfigMap) (*v1.Pod, error), found: func(context.Context, *v1.ConfigMap) (*v1.Pod, string)`, - }, { name: "ReflectChildStatusOnParent missing", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "ReflectChildStatusOnParent missing", ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -621,75 +433,10 @@ func TestChildReconciler_validate(t *testing.T) { }, shouldErr: `ChildReconciler "ReflectChildStatusOnParent missing" must implement ReflectChildStatusOnParent`, }, - { - name: "ReflectChildStatusOnParent num in", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ReflectChildStatusOnParent num in", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func() {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "ReflectChildStatusOnParent num in" must implement ReflectChildStatusOnParent: func(*v1.ConfigMap, *v1.Pod, error), found: func()`, - }, - { - name: "ReflectChildStatusOnParent in 0", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ReflectChildStatusOnParent in 0", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.Secret, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "ReflectChildStatusOnParent in 0" must implement ReflectChildStatusOnParent: func(*v1.ConfigMap, *v1.Pod, error), found: func(*v1.Secret, *v1.Pod, error)`, - }, - { - name: "ReflectChildStatusOnParent in 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ReflectChildStatusOnParent in 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Secret, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "ReflectChildStatusOnParent in 1" must implement ReflectChildStatusOnParent: func(*v1.ConfigMap, *v1.Pod, error), found: func(*v1.ConfigMap, *v1.Secret, error)`, - }, - { - name: "ReflectChildStatusOnParent in 2", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ReflectChildStatusOnParent in 2", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, ctx context.Context) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "ReflectChildStatusOnParent in 2" must implement ReflectChildStatusOnParent: func(*v1.ConfigMap, *v1.Pod, error), found: func(*v1.ConfigMap, *v1.Pod, context.Context)`, - }, - { - name: "ReflectChildStatusOnParent num out", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ReflectChildStatusOnParent num out", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) error { return nil }, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "ReflectChildStatusOnParent num out" must implement ReflectChildStatusOnParent: func(*v1.ConfigMap, *v1.Pod, error), found: func(*v1.ConfigMap, *v1.Pod, error) error`, - }, { name: "ListOptions", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + 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 }, @@ -698,80 +445,10 @@ func TestChildReconciler_validate(t *testing.T) { ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, }, }, - { - name: "ListOptions num in", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ListOptions num in", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - ListOptions: func() []client.ListOption { return []client.ListOption{} }, - }, - shouldErr: `ChildReconciler "ListOptions num in" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func() []client.ListOption`, - }, - { - name: "ListOptions in 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ListOptions in 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - ListOptions: func(child *corev1.Secret, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} }, - }, - shouldErr: `ChildReconciler "ListOptions in 1" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(*v1.Secret, *v1.ConfigMap) []client.ListOption`, - }, - { - name: "ListOptions in 2", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ListOptions in 2", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - ListOptions: func(ctx context.Context, parent *corev1.Secret) []client.ListOption { return []client.ListOption{} }, - }, - shouldErr: `ChildReconciler "ListOptions in 2" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.Secret) []client.ListOption`, - }, - { - name: "ListOptions num out", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ListOptions num out", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) {}, - }, - shouldErr: `ChildReconciler "ListOptions num out" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.ConfigMap)`, - }, - { - name: "ListOptions out 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "ListOptions out 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) client.ListOptions { return client.ListOptions{} }, - }, - shouldErr: `ChildReconciler "ListOptions out 1" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.ConfigMap) client.ListOptions`, - }, { name: "Finalizer without OurChild", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "Finalizer without OurChild", ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -785,7 +462,7 @@ func TestChildReconciler_validate(t *testing.T) { { name: "SkipOwnerReference without OurChild", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + reconciler: &ChildReconciler[*corev1.ConfigMap, *corev1.Pod, *corev1.PodList]{ Name: "SkipOwnerReference without OurChild", ChildType: &corev1.Pod{}, ChildListType: &corev1.PodList{}, @@ -799,7 +476,7 @@ func TestChildReconciler_validate(t *testing.T) { { name: "OurChild", parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ + 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 }, @@ -808,76 +485,6 @@ func TestChildReconciler_validate(t *testing.T) { OurChild: func(parent *corev1.ConfigMap, child *corev1.Pod) bool { return false }, }, }, - { - name: "OurChild num in", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "OurChild num in", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - OurChild: func() bool { return false }, - }, - shouldErr: `ChildReconciler "OurChild num in" must implement OurChild: nil | func(*v1.ConfigMap, *v1.Pod) bool, found: func() bool`, - }, - { - name: "OurChild in 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "OurChild in 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - OurChild: func(parent *corev1.ConfigMap, child *corev1.Secret) bool { return false }, - }, - shouldErr: `ChildReconciler "OurChild in 1" must implement OurChild: nil | func(*v1.ConfigMap, *v1.Pod) bool, found: func(*v1.ConfigMap, *v1.Secret) bool`, - }, - { - name: "OurChild in 2", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "OurChild in 2", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - OurChild: func(parent *corev1.Secret, child *corev1.Pod) bool { return false }, - }, - shouldErr: `ChildReconciler "OurChild in 2" must implement OurChild: nil | func(*v1.ConfigMap, *v1.Pod) bool, found: func(*v1.Secret, *v1.Pod) bool`, - }, - { - name: "OurChild num out", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "OurChild num out", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - OurChild: func(parent *corev1.ConfigMap, child *corev1.Pod) {}, - }, - shouldErr: `ChildReconciler "OurChild num out" must implement OurChild: nil | func(*v1.ConfigMap, *v1.Pod) bool, found: func(*v1.ConfigMap, *v1.Pod)`, - }, - { - name: "OurChild out 1", - parent: &corev1.ConfigMap{}, - reconciler: &ChildReconciler{ - Name: "OurChild out 1", - ChildType: &corev1.Pod{}, - ChildListType: &corev1.PodList{}, - DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil }, - ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {}, - MergeBeforeUpdate: func(current, desired *corev1.Pod) {}, - OurChild: func(parent *corev1.ConfigMap, child *corev1.Pod) *corev1.Pod { return child }, - }, - shouldErr: `ChildReconciler "OurChild out 1" must implement OurChild: nil | func(*v1.ConfigMap, *v1.Pod) bool, found: func(*v1.ConfigMap, *v1.Pod) *v1.Pod`, - }, } for _, c := range tests { @@ -895,47 +502,31 @@ func TestCastResource_validate(t *testing.T) { tests := []struct { name string resource client.Object - reconciler *CastResource + reconciler *CastResource[*corev1.ConfigMap, *corev1.Secret] shouldErr string }{ { name: "empty", resource: &corev1.ConfigMap{}, - reconciler: &CastResource{}, - shouldErr: `CastResource "" must define Type`, + reconciler: &CastResource[*corev1.ConfigMap, *corev1.Secret]{}, + shouldErr: `CastResource "" must define Reconciler`, }, { name: "valid", resource: &corev1.ConfigMap{}, - reconciler: &CastResource{ - Type: &corev1.Secret{}, - Reconciler: &SyncReconciler{ - Sync: func(ctx context.Context, resource *corev1.Secret) error { - return nil - }, - }, - }, - }, - { - name: "missing type", - resource: &corev1.ConfigMap{}, - reconciler: &CastResource{ - Name: "missing type", - Type: nil, - Reconciler: &SyncReconciler{ + reconciler: &CastResource[*corev1.ConfigMap, *corev1.Secret]{ + Reconciler: &SyncReconciler[*corev1.Secret]{ Sync: func(ctx context.Context, resource *corev1.Secret) error { return nil }, }, }, - shouldErr: `CastResource "missing type" must define Type`, }, { name: "missing reconciler", resource: &corev1.ConfigMap{}, - reconciler: &CastResource{ + reconciler: &CastResource[*corev1.ConfigMap, *corev1.Secret]{ Name: "missing reconciler", - Type: &corev1.Secret{}, Reconciler: nil, }, shouldErr: `CastResource "missing reconciler" must define Reconciler`, @@ -961,20 +552,20 @@ func TestWithConfig_validate(t *testing.T) { tests := []struct { name string resource client.Object - reconciler *WithConfig + reconciler *WithConfig[*corev1.ConfigMap] shouldErr string }{ { name: "empty", resource: &corev1.ConfigMap{}, - reconciler: &WithConfig{}, + reconciler: &WithConfig[*corev1.ConfigMap]{}, shouldErr: `WithConfig "" must define Config`, }, { name: "valid", resource: &corev1.ConfigMap{}, - reconciler: &WithConfig{ - Reconciler: &Sequence{}, + reconciler: &WithConfig[*corev1.ConfigMap]{ + Reconciler: &Sequence[*corev1.ConfigMap]{}, Config: func(ctx context.Context, c Config) (Config, error) { return config, nil }, @@ -983,16 +574,16 @@ func TestWithConfig_validate(t *testing.T) { { name: "missing config", resource: &corev1.ConfigMap{}, - reconciler: &WithConfig{ + reconciler: &WithConfig[*corev1.ConfigMap]{ Name: "missing config", - Reconciler: &Sequence{}, + Reconciler: &Sequence[*corev1.ConfigMap]{}, }, shouldErr: `WithConfig "missing config" must define Config`, }, { name: "missing reconciler", resource: &corev1.ConfigMap{}, - reconciler: &WithConfig{ + reconciler: &WithConfig[*corev1.ConfigMap]{ Name: "missing reconciler", Config: func(ctx context.Context, c Config) (Config, error) { return config, nil @@ -1017,36 +608,36 @@ func TestWithFinalizer_validate(t *testing.T) { tests := []struct { name string resource client.Object - reconciler *WithFinalizer + reconciler *WithFinalizer[*corev1.ConfigMap] shouldErr string }{ { name: "empty", resource: &corev1.ConfigMap{}, - reconciler: &WithFinalizer{}, + reconciler: &WithFinalizer[*corev1.ConfigMap]{}, shouldErr: `WithFinalizer "" must define Finalizer`, }, { name: "valid", resource: &corev1.ConfigMap{}, - reconciler: &WithFinalizer{ - Reconciler: &Sequence{}, + reconciler: &WithFinalizer[*corev1.ConfigMap]{ + Reconciler: &Sequence[*corev1.ConfigMap]{}, Finalizer: "my-finalizer", }, }, { name: "missing finalizer", resource: &corev1.ConfigMap{}, - reconciler: &WithFinalizer{ + reconciler: &WithFinalizer[*corev1.ConfigMap]{ Name: "missing finalizer", - Reconciler: &Sequence{}, + Reconciler: &Sequence[*corev1.ConfigMap]{}, }, shouldErr: `WithFinalizer "missing finalizer" must define Finalizer`, }, { name: "missing reconciler", resource: &corev1.ConfigMap{}, - reconciler: &WithFinalizer{ + reconciler: &WithFinalizer[*corev1.ConfigMap]{ Name: "missing reconciler", Finalizer: "my-finalizer", }, @@ -1068,161 +659,54 @@ func TestWithFinalizer_validate(t *testing.T) { func TestResourceManager_validate(t *testing.T) { tests := []struct { name string - reconciler *ResourceManager + reconciler *ResourceManager[*resources.TestResource] shouldErr string expectedLogs []string }{ { name: "empty", - reconciler: &ResourceManager{}, - shouldErr: `ResourceManager "" must define Type`, + reconciler: &ResourceManager[*resources.TestResource]{}, + shouldErr: `ResourceManager "" must define MergeBeforeUpdate`, }, { name: "valid", - reconciler: &ResourceManager{ + reconciler: &ResourceManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, }, { name: "Type missing", - reconciler: &ResourceManager{ + reconciler: &ResourceManager[*resources.TestResource]{ Name: "Type missing", // Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, - shouldErr: `ResourceManager "Type missing" must define Type`, }, { name: "MergeBeforeUpdate missing", - reconciler: &ResourceManager{ + reconciler: &ResourceManager[*resources.TestResource]{ Name: "MergeBeforeUpdate missing", Type: &resources.TestResource{}, // MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, }, shouldErr: `ResourceManager "MergeBeforeUpdate missing" must define MergeBeforeUpdate`, }, - { - name: "MergeBeforeUpdate num in", - reconciler: &ResourceManager{ - Name: "MergeBeforeUpdate num in", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func() {}, - }, - shouldErr: `ResourceManager "MergeBeforeUpdate num in" must implement MergeBeforeUpdate: func(*resources.TestResource, *resources.TestResource), found: func()`, - }, - { - name: "MergeBeforeUpdate in 0", - reconciler: &ResourceManager{ - Name: "MergeBeforeUpdate in 0", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current *corev1.Pod, desired *resources.TestResource) {}, - }, - shouldErr: `ResourceManager "MergeBeforeUpdate in 0" must implement MergeBeforeUpdate: func(*resources.TestResource, *resources.TestResource), found: func(*v1.Pod, *resources.TestResource)`, - }, - { - name: "MergeBeforeUpdate in 1", - reconciler: &ResourceManager{ - Name: "MergeBeforeUpdate in 1", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current *resources.TestResource, desired *corev1.Pod) {}, - }, - shouldErr: `ResourceManager "MergeBeforeUpdate in 1" must implement MergeBeforeUpdate: func(*resources.TestResource, *resources.TestResource), found: func(*resources.TestResource, *v1.Pod)`, - }, - { - name: "MergeBeforeUpdate num out", - reconciler: &ResourceManager{ - Name: "MergeBeforeUpdate num out", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) error { return nil }, - }, - shouldErr: `ResourceManager "MergeBeforeUpdate num out" must implement MergeBeforeUpdate: func(*resources.TestResource, *resources.TestResource), found: func(*resources.TestResource, *resources.TestResource) error`, - }, { name: "HarmonizeImmutableFields", - reconciler: &ResourceManager{ + reconciler: &ResourceManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, HarmonizeImmutableFields: func(current, desired *resources.TestResource) {}, }, }, - { - name: "HarmonizeImmutableFields num in", - reconciler: &ResourceManager{ - Name: "HarmonizeImmutableFields num in", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - HarmonizeImmutableFields: func() {}, - }, - shouldErr: `ResourceManager "HarmonizeImmutableFields num in" must implement HarmonizeImmutableFields: nil | func(*resources.TestResource, *resources.TestResource), found: func()`, - }, - { - name: "HarmonizeImmutableFields in 0", - reconciler: &ResourceManager{ - Name: "HarmonizeImmutableFields in 0", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - HarmonizeImmutableFields: func(current *corev1.Pod, desired *resources.TestResource) {}, - }, - shouldErr: `ResourceManager "HarmonizeImmutableFields in 0" must implement HarmonizeImmutableFields: nil | func(*resources.TestResource, *resources.TestResource), found: func(*v1.Pod, *resources.TestResource)`, - }, - { - name: "HarmonizeImmutableFields in 1", - reconciler: &ResourceManager{ - Name: "HarmonizeImmutableFields in 1", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - HarmonizeImmutableFields: func(current *resources.TestResource, desired *corev1.Pod) {}, - }, - shouldErr: `ResourceManager "HarmonizeImmutableFields in 1" must implement HarmonizeImmutableFields: nil | func(*resources.TestResource, *resources.TestResource), found: func(*resources.TestResource, *v1.Pod)`, - }, - { - name: "HarmonizeImmutableFields num out", - reconciler: &ResourceManager{ - Name: "HarmonizeImmutableFields num out", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - HarmonizeImmutableFields: func(current, desired *resources.TestResource) error { return nil }, - }, - shouldErr: `ResourceManager "HarmonizeImmutableFields num out" must implement HarmonizeImmutableFields: nil | func(*resources.TestResource, *resources.TestResource), found: func(*resources.TestResource, *resources.TestResource) error`, - }, { name: "Sanitize", - reconciler: &ResourceManager{ - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - Sanitize: func(child *resources.TestResource) resources.TestResourceSpec { return child.Spec }, - }, - }, - { - name: "Sanitize num in", - reconciler: &ResourceManager{ - Name: "Sanitize num in", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - Sanitize: func() resources.TestResourceSpec { return resources.TestResourceSpec{} }, - }, - shouldErr: `ResourceManager "Sanitize num in" must implement Sanitize: nil | func(*resources.TestResource) interface{}, found: func() resources.TestResourceSpec`, - }, - { - name: "Sanitize in 1", - reconciler: &ResourceManager{ - Name: "Sanitize in 1", - Type: &resources.TestResource{}, - MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - Sanitize: func(child *corev1.Pod) corev1.PodSpec { return child.Spec }, - }, - shouldErr: `ResourceManager "Sanitize in 1" must implement Sanitize: nil | func(*resources.TestResource) interface{}, found: func(*v1.Pod) v1.PodSpec`, - }, - { - name: "Sanitize num out", - reconciler: &ResourceManager{ - Name: "Sanitize num out", + reconciler: &ResourceManager[*resources.TestResource]{ Type: &resources.TestResource{}, MergeBeforeUpdate: func(current, desired *resources.TestResource) {}, - Sanitize: func(child *resources.TestResource) {}, + Sanitize: func(child *resources.TestResource) interface{} { return child.Spec }, }, - shouldErr: `ResourceManager "Sanitize num out" must implement Sanitize: nil | func(*resources.TestResource) interface{}, found: func(*resources.TestResource)`, }, } diff --git a/reconcilers/webhook.go b/reconcilers/webhook.go index 6ffc5d7..b352a20 100644 --- a/reconcilers/webhook.go +++ b/reconcilers/webhook.go @@ -10,8 +10,10 @@ import ( "encoding/json" "fmt" "net/http" + "sync" "github.com/go-logr/logr" + "github.com/vmware-labs/reconciler-runtime/internal" "gomodules.xyz/jsonpatch/v3" admissionv1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -19,7 +21,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" crlog "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -41,35 +42,49 @@ import ( // // If the resource being reconciled is mutated and the response does not already define a patch, a // json patch is computed for the mutation and set on the response. -type AdmissionWebhookAdapter struct { +// +// If the webhook can handle multiple types, use *unstructured.Unstructured as the generic type. +type AdmissionWebhookAdapter[Type client.Object] struct { // Name used to identify this reconciler. Defaults to `{Type}AdmissionWebhookAdapter`. Ideally // unique, but not required to be so. // // +optional Name string - // Type of resource to reconcile. If the webhook can handle multiple types, use - // *unstructured.Unstructured{}. - Type client.Object + // Type of resource to reconcile. Required when the generic type is not a struct. + // + // +optional + Type Type // Reconciler is called for each reconciler request with the resource being reconciled. // Typically, Reconciler is a Sequence of multiple SubReconcilers. - Reconciler SubReconciler + Reconciler SubReconciler[Type] Config Config + + lazyInit sync.Once } -func (r *AdmissionWebhookAdapter) Build() *admission.Webhook { - name := r.Name - if name == "" { - name = fmt.Sprintf("%sAdmissionWebhookAdapter", typeName(r.Type)) - } +func (r *AdmissionWebhookAdapter[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("%sAdmissionWebhookAdapter", typeName(r.Type)) + } + }) +} + +func (r *AdmissionWebhookAdapter[T]) Build() *admission.Webhook { + r.init() return &admission.Webhook{ Handler: r, WithContextFunc: func(ctx context.Context, req *http.Request) context.Context { log := crlog.FromContext(ctx). WithName("controller-runtime.webhook.webhooks"). - WithName(name). + WithName(r.Name). WithValues( "webhook", req.URL.Path, ) @@ -89,7 +104,9 @@ func (r *AdmissionWebhookAdapter) Build() *admission.Webhook { } // Handle implements admission.Handler -func (r *AdmissionWebhookAdapter) Handle(ctx context.Context, req admission.Request) admission.Response { +func (r *AdmissionWebhookAdapter[T]) Handle(ctx context.Context, req admission.Request) admission.Response { + r.init() + log := logr.FromContextOrDiscard(ctx). WithValues( "UID", req.UID, @@ -111,7 +128,7 @@ func (r *AdmissionWebhookAdapter) Handle(ctx context.Context, req admission.Requ ctx = StashAdmissionResponse(ctx, resp) // defined for compatibility since this is not a reconciler - ctx = StashRequest(ctx, reconcile.Request{ + ctx = StashRequest(ctx, Request{ NamespacedName: types.NamespacedName{ Namespace: req.Namespace, Name: req.Name, @@ -132,10 +149,10 @@ func (r *AdmissionWebhookAdapter) Handle(ctx context.Context, req admission.Requ return *resp } -func (r *AdmissionWebhookAdapter) reconcile(ctx context.Context, req admission.Request, resp *admission.Response) error { +func (r *AdmissionWebhookAdapter[T]) reconcile(ctx context.Context, req admission.Request, resp *admission.Response) error { log := logr.FromContextOrDiscard(ctx) - resource := r.Type.DeepCopyObject().(client.Object) + resource := r.Type.DeepCopyObject().(T) resourceBytes := req.Object.Raw if req.Operation == admissionv1.Delete { resourceBytes = req.OldObject.Raw @@ -144,7 +161,7 @@ func (r *AdmissionWebhookAdapter) reconcile(ctx context.Context, req admission.R return err } - if defaulter, ok := resource.(webhook.Defaulter); ok { + if defaulter, ok := client.Object(resource).(webhook.Defaulter); ok { // resource.Default() defaulter.Default() } diff --git a/reconcilers/webhook_test.go b/reconcilers/webhook_test.go index 1fe79fd..4cf934c 100644 --- a/reconcilers/webhook_test.go +++ b/reconcilers/webhook_test.go @@ -77,8 +77,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { AdmissionResponse: response.DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return nil }, @@ -105,8 +105,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { }, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { resource.Spec.Fields = map[string]string{ "hello": "world", @@ -133,8 +133,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return fmt.Errorf("reconcile error") }, @@ -160,8 +160,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { return nil }, @@ -186,8 +186,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { AdmissionResponse: response.DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if resource.Spec.Fields["hello"] != "world" { t.Errorf("expected field %q to have value %q", "hello", "world") @@ -234,8 +234,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { }, }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { c := reconcilers.RetrieveConfigOrDie(ctx) cm := &corev1.ConfigMap{} @@ -259,8 +259,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { AdmissionResponse: response.DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, _ *resources.TestResource) error { actualRequest := reconcilers.RetrieveAdmissionRequest(ctx) expectedRequest := admission.Request{ @@ -305,9 +305,9 @@ func TestAdmissionWebhookAdapter(t *testing.T) { AdmissionResponse: response.DieRelease(), }, Metadata: map[string]interface{}{ - "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return reconcilers.Sequence{ - &reconcilers.SyncReconciler{ + "SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return reconcilers.Sequence[*resources.TestResource]{ + &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, _ *resources.TestResource) error { // StashValue will panic if context is not setup for stashing reconcilers.StashValue(ctx, reconcilers.StashKey("greeting"), "hello") @@ -315,7 +315,7 @@ func TestAdmissionWebhookAdapter(t *testing.T) { return nil }, }, - &reconcilers.SyncReconciler{ + &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, _ *resources.TestResource) error { // StashValue will panic if context is not setup for stashing greeting := reconcilers.RetrieveValue(ctx, reconcilers.StashKey("greeting")) @@ -344,8 +344,8 @@ func TestAdmissionWebhookAdapter(t *testing.T) { value := "test-value" ctx = context.WithValue(ctx, key, value) - tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler { - return &reconcilers.SyncReconciler{ + tc.Metadata["SubReconciler"] = func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ Sync: func(ctx context.Context, resource *resources.TestResource) error { if v := ctx.Value(key); v != value { t.Errorf("expected %s to be in context", key) @@ -367,9 +367,9 @@ func TestAdmissionWebhookAdapter(t *testing.T) { } wts.Run(t, scheme, func(t *testing.T, wtc *rtesting.AdmissionWebhookTestCase, c reconcilers.Config) *admission.Webhook { - return (&reconcilers.AdmissionWebhookAdapter{ + return (&reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{ Type: &resources.TestResource{}, - Reconciler: wtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler)(t, c), + Reconciler: wtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c), Config: c, }).Build() }) diff --git a/testing/aliases.go b/testing/aliases.go index 010d12d..7ce31ae 100644 --- a/testing/aliases.go +++ b/testing/aliases.go @@ -7,7 +7,6 @@ package testing import ( clientgotesting "k8s.io/client-go/testing" - "k8s.io/utils/pointer" ) type Reactor = clientgotesting.Reactor @@ -21,14 +20,3 @@ type UpdateAction = clientgotesting.UpdateAction type PatchAction = clientgotesting.PatchAction type DeleteAction = clientgotesting.DeleteAction type DeleteCollectionAction = clientgotesting.DeleteCollectionAction - -var ( - // Deprecated use pointer.String - StringPtr = pointer.String - // Deprecated use pointer.Bool - BoolPtr = pointer.Bool - // Deprecated use pointer.Int32 - Int32Ptr = pointer.Int32 - // Deprecated use pointer.Int64 - Int64Ptr = pointer.Int64 -) diff --git a/testing/assert.go b/testing/assert.go deleted file mode 100644 index db87272..0000000 --- a/testing/assert.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2019 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -package testing - -import ( - "fmt" - "testing" - - controllerruntime "sigs.k8s.io/controller-runtime" -) - -// Deprecated -func AssertErrorEqual(expected error) VerifyFunc { - return func(t *testing.T, result controllerruntime.Result, err error) { - if err != expected { - t.Errorf("Unexpected error: expected %v, actual %v", expected, err) - } - } -} - -// Deprecated -func AssertErrorMessagef(message string, a ...interface{}) VerifyFunc { - return func(t *testing.T, result controllerruntime.Result, err error) { - expected := fmt.Sprintf(message, a...) - actual := "" - if err != nil { - actual = err.Error() - } - if actual != expected { - t.Errorf("Unexpected error message: expected %v, actual %v", expected, actual) - } - } -} diff --git a/testing/client.go b/testing/client.go index 29b3539..14d398d 100644 --- a/testing/client.go +++ b/testing/client.go @@ -17,7 +17,6 @@ import ( clientgotesting "k8s.io/client-go/testing" ref "k8s.io/client-go/tools/reference" "sigs.k8s.io/controller-runtime/pkg/client" - fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" ) type clientWrapper struct { @@ -35,15 +34,6 @@ type clientWrapper struct { var _ client.Client = &clientWrapper{} -// Deprecated NewFakeClient use NewFakeClientWrapper -func NewFakeClient(scheme *runtime.Scheme, objs ...client.Object) *clientWrapper { - builder := fakeclient.NewClientBuilder() - builder = builder.WithScheme(scheme) - builder = builder.WithObjects(prepareObjects(objs)...) - - return NewFakeClientWrapper(builder.Build()) -} - func NewFakeClientWrapper(client client.Client) *clientWrapper { c := &clientWrapper{ client: client, diff --git a/testing/config.go b/testing/config.go index 1ec97bd..c2c51ff 100644 --- a/testing/config.go +++ b/testing/config.go @@ -11,7 +11,6 @@ import ( "sync" "testing" - "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/vmware-labs/reconciler-runtime/reconcilers" @@ -20,6 +19,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -128,8 +128,6 @@ func (c *ExpectConfig) Config() reconcilers.Config { APIReader: c.apiReader, Recorder: c.recorder, Tracker: c.tracker, - // Log is deprecated and should not be used. Setting the discard logger until it is fully removed. - Log: logr.Discard(), } } @@ -415,13 +413,13 @@ var ( if s == nil || s.Empty() { return nil } - return StringPtr(s.String()) + return pointer.String(s.String()) }) NormalizeFieldSelector = cmp.Transformer("fields.Selector", func(s fields.Selector) *string { if s == nil || s.Empty() { return nil } - return StringPtr(s.String()) + return pointer.String(s.String()) }) ) diff --git a/testing/reconciler.go b/testing/reconciler.go index 3fbe6f8..3ff2388 100644 --- a/testing/reconciler.go +++ b/testing/reconciler.go @@ -14,8 +14,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/vmware-labs/reconciler-runtime/reconcilers" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -35,10 +33,8 @@ type ReconcilerTestCase struct { // inputs - // Deprecated use Request - Key types.NamespacedName // Request identifies the object to be reconciled - Request controllerruntime.Request + Request reconcilers.Request // WithReactors installs each ReactionFunc into each fake clientset. ReactionFuncs intercept // each call to the clientset providing the ability to mutate the resource or inject an error. WithReactors []ReactionFunc @@ -82,7 +78,7 @@ type ReconcilerTestCase struct { // ShouldErr is true if and only if reconciliation is expected to return an error ShouldErr bool // ExpectedResult is compared to the result returned from the reconciler if there was no error - ExpectedResult controllerruntime.Result + ExpectedResult reconcilers.Result // Verify provides the reconciliation Result and error for custom assertions Verify VerifyFunc @@ -99,7 +95,7 @@ type ReconcilerTestCase struct { } // VerifyFunc is a verification function for a reconciler's result -type VerifyFunc func(t *testing.T, result controllerruntime.Result, err error) +type VerifyFunc func(t *testing.T, result reconcilers.Result, err error) // VerifyStashedValueFunc is a verification function for the entries in the stash type VerifyStashedValueFunc func(t *testing.T, key reconcilers.StashKey, expected, actual interface{}) @@ -184,11 +180,7 @@ func (tc *ReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory r := factory(t, tc, expectConfig.Config()) // Run the Reconcile we're testing. - request := tc.Request - if request == (controllerruntime.Request{}) { - request.NamespacedName = tc.Key - } - result, err := r.Reconcile(ctx, request) + result, err := r.Reconcile(ctx, tc.Request) if (err != nil) != tc.ShouldErr { t.Errorf("Reconcile() error = %v, ShouldErr %v", err, tc.ShouldErr) @@ -210,7 +202,7 @@ func (tc *ReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory } } -func normalizeResult(result controllerruntime.Result) controllerruntime.Result { +func normalizeResult(result reconcilers.Result) reconcilers.Result { // RequeueAfter implies Requeue, no need to set both if result.RequeueAfter != 0 { result.Requeue = false diff --git a/testing/subreconciler.go b/testing/subreconciler.go index 2c68eca..f1f1410 100644 --- a/testing/subreconciler.go +++ b/testing/subreconciler.go @@ -13,17 +13,16 @@ import ( logrtesting "github.com/go-logr/logr/testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/vmware-labs/reconciler-runtime/internal" "github.com/vmware-labs/reconciler-runtime/reconcilers" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // SubReconcilerTestCase holds a single testcase of a sub reconciler test. -type SubReconcilerTestCase struct { +type SubReconcilerTestCase[Type client.Object] struct { // Name is a descriptive name for this test suitable as a first argument to t.Run() Name string // Focus is true if and only if only this and any other focused tests are to be executed. @@ -36,10 +35,8 @@ type SubReconcilerTestCase struct { // inputs - // Deprecated use Resource - Parent client.Object // Resource is the initial object passed to the sub reconciler - Resource client.Object + Resource Type // GivenStashedValues adds these items to the stash passed into the reconciler. Factories are resolved to their object. GivenStashedValues map[reconcilers.StashKey]interface{} // WithClientBuilder allows a test to modify the fake client initialization. @@ -56,10 +53,8 @@ type SubReconcilerTestCase struct { // side effects - // Deprecated use ExpectResource - ExpectParent client.Object // ExpectResource is the expected reconciled resource as mutated after the sub reconciler, or nil if no modification - ExpectResource client.Object + ExpectResource Type // ExpectStashedValues ensures each value is stashed. Values in the stash that are not expected are ignored. Factories are resolved to their object. ExpectStashedValues map[reconcilers.StashKey]interface{} // VerifyStashedValue is an optional, custom verification function for stashed values @@ -92,7 +87,7 @@ type SubReconcilerTestCase struct { // used to indicate the reconciler is misconfigured. ShouldPanic bool // ExpectedResult is compared to the result returned from the reconciler if there was no error - ExpectedResult controllerruntime.Result + ExpectedResult reconcilers.Result // Verify provides the reconciliation Result and error for custom assertions Verify VerifyFunc @@ -101,21 +96,21 @@ type SubReconcilerTestCase struct { // Prepare is called before the reconciler is executed. It is intended to prepare the broader // environment before the specific test case is executed. For example, setting mock // expectations, or adding values to the context, - Prepare func(t *testing.T, ctx context.Context, tc *SubReconcilerTestCase) (context.Context, error) + Prepare func(t *testing.T, ctx context.Context, tc *SubReconcilerTestCase[Type]) (context.Context, error) // CleanUp is called after the test case is finished and all defined assertions complete. // It is intended to clean up any state created in the Prepare step or during the test // execution, or to make assertions for mocks. - CleanUp func(t *testing.T, ctx context.Context, tc *SubReconcilerTestCase) error + CleanUp func(t *testing.T, ctx context.Context, tc *SubReconcilerTestCase[Type]) error } // SubReconcilerTests represents a map of reconciler test cases. The map key is the name of each // test case. Test cases are executed in random order. -type SubReconcilerTests map[string]SubReconcilerTestCase +type SubReconcilerTests[Type client.Object] map[string]SubReconcilerTestCase[Type] // Run executes the test cases. -func (rt SubReconcilerTests) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory) { +func (rt SubReconcilerTests[T]) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory[T]) { t.Helper() - rts := SubReconcilerTestSuite{} + rts := SubReconcilerTestSuite[T]{} for name, rtc := range rt { rtc.Name = name rts = append(rts, rtc) @@ -125,10 +120,10 @@ func (rt SubReconcilerTests) Run(t *testing.T, scheme *runtime.Scheme, factory S // SubReconcilerTestSuite represents a list of subreconciler test cases. The test cases are // executed in order. -type SubReconcilerTestSuite []SubReconcilerTestCase +type SubReconcilerTestSuite[Type client.Object] []SubReconcilerTestCase[Type] // Run executes the test case. -func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory) { +func (tc *SubReconcilerTestCase[T]) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory[T]) { t.Helper() if tc.Skip { t.SkipNow() @@ -169,16 +164,6 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto }() } - // TODO remove deprecation shim - if tc.Resource == nil && tc.Parent != nil { - t.Log("Parent field is deprecated for SubReconcilerTestCase, use Resource instead") - tc.Resource = tc.Parent - } - if tc.ExpectResource == nil && tc.ExpectParent != nil { - t.Log("ExpectParent field is deprecated for SubReconcilerTestCase, use ExpectResource instead") - tc.ExpectResource = tc.ExpectParent - } - expectConfig := &ExpectConfig{ Name: "default", Scheme: scheme, @@ -209,12 +194,12 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto ctx = reconcilers.StashConfig(ctx, c) ctx = reconcilers.StashOriginalConfig(ctx, c) - resource := tc.Resource.DeepCopyObject().(client.Object) + resource := tc.Resource.DeepCopyObject().(T) if resource.GetResourceVersion() == "" { // this value is also set by the test client when resource are added as givens resource.SetResourceVersion("999") } - ctx = reconcilers.StashRequest(ctx, reconcile.Request{ + ctx = reconcilers.StashRequest(ctx, reconcilers.Request{ NamespacedName: types.NamespacedName{Namespace: resource.GetNamespace(), Name: resource.GetName()}, }) ctx = reconcilers.StashOriginalResourceType(ctx, resource.DeepCopyObject().(client.Object)) @@ -228,7 +213,7 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto ctx = reconcilers.StashAdditionalConfigs(ctx, configs) // Run the Reconcile we're testing. - result, err := func(ctx context.Context, resource client.Object) (reconcile.Result, error) { + result, err := func(ctx context.Context, resource T) (reconcilers.Result, error) { if tc.ShouldPanic { defer func() { if r := recover(); r == nil { @@ -256,7 +241,7 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto // compare resource expectedResource := tc.Resource.DeepCopyObject().(client.Object) - if tc.ExpectResource != nil { + if !internal.IsNil(tc.ExpectResource) { expectedResource = tc.ExpectResource.DeepCopyObject().(client.Object) } if expectedResource.GetResourceVersion() == "" { @@ -283,9 +268,9 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto } // Run executes the subreconciler test suite. -func (ts SubReconcilerTestSuite) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory) { +func (ts SubReconcilerTestSuite[T]) Run(t *testing.T, scheme *runtime.Scheme, factory SubReconcilerFactory[T]) { t.Helper() - focused := SubReconcilerTestSuite{} + focused := SubReconcilerTestSuite[T]{} for _, test := range ts { if test.Focus { focused = append(focused, test) @@ -310,4 +295,4 @@ func (ts SubReconcilerTestSuite) Run(t *testing.T, scheme *runtime.Scheme, facto // SubReconcilerFactory returns a Reconciler.Interface to perform reconciliation of a test case, // ActionRecorderList/EventList to capture k8s actions/events produced during reconciliation // and FakeStatsReporter to capture stats. -type SubReconcilerFactory func(t *testing.T, rtc *SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler +type SubReconcilerFactory[Type client.Object] func(t *testing.T, rtc *SubReconcilerTestCase[Type], c reconcilers.Config) reconcilers.SubReconciler[Type] diff --git a/validation/docs.go b/validation/docs.go deleted file mode 100644 index 8c173fc..0000000 --- a/validation/docs.go +++ /dev/null @@ -1,7 +0,0 @@ -/* -Copyright 20222 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -// Deprecated use k8s.io/apimachinery/pkg/util/validation/field directly -package validation diff --git a/validation/fielderrors.go b/validation/fielderrors.go deleted file mode 100644 index 1b61807..0000000 --- a/validation/fielderrors.go +++ /dev/null @@ -1,156 +0,0 @@ -/* -Copyright 2019 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -package validation - -import ( - "context" - "fmt" - "strings" - - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// Deprecated CurrentField is an empty string representing an empty path to the current field. -const CurrentField = "" - -type FieldValidator interface { - Validate() FieldErrors -} - -// Deprecated FieldErrors extends an ErrorList to compose helper methods to compose field paths. -type FieldErrors field.ErrorList - -// Deprecated Also appends additional field errors to the current set of errors. -func (e FieldErrors) Also(errs ...FieldErrors) FieldErrors { - aggregate := e - for _, err := range errs { - aggregate = append(aggregate, err...) - } - return aggregate -} - -// Deprecated ViaField prepends the path of each error with the field's key using a dot notation separator (e.g. '.foo') -func (e FieldErrors) ViaField(key string) FieldErrors { - errs := make(FieldErrors, len(e)) - for i, err := range e { - newField := key - if !strings.HasPrefix(err.Field, "[") { - newField = newField + "." - } - if err.Field != "[]" { - newField = newField + err.Field - } - errs[i] = &field.Error{ - Type: err.Type, - Field: newField, - BadValue: err.BadValue, - Detail: err.Detail, - } - } - return errs -} - -// Deprecated ViaIndex prepends the path of each error with the field's index using square bracket separators (e.g. '[0]'). -func (e FieldErrors) ViaIndex(index int) FieldErrors { - errs := make(FieldErrors, len(e)) - for i, err := range e { - newField := fmt.Sprintf("[%d]", index) - if !strings.HasPrefix(err.Field, "[") { - newField = newField + "." - } - if err.Field != "[]" { - newField = newField + err.Field - } - errs[i] = &field.Error{ - Type: err.Type, - Field: newField, - BadValue: err.BadValue, - Detail: err.Detail, - } - } - return errs -} - -// Deprecated ViaFieldIndex prepends the path of each error with the fields key and index (e.g. '.foo[0]'). -func (e FieldErrors) ViaFieldIndex(key string, index int) FieldErrors { - return e.ViaIndex(index).ViaField(key) -} - -// Deprecated ErrorList converts a FieldErrors to an api machinery field ErrorList -func (e FieldErrors) ErrorList() field.ErrorList { - list := make(field.ErrorList, len(e)) - for i := range e { - list[i] = e[i] - } - return list -} - -// Deprecated ToAggregate combines the field errors into a single go error, or nil if there are no errors. -func (e FieldErrors) ToAggregate() error { - l := e.ErrorList() - if len(l) == 0 { - return nil - } - return l.ToAggregate() -} - -// Deprecated -type Validatable = interface { - Validate(context.Context) FieldErrors -} - -// Deprecated ErrDisallowedFields wraps a forbidden error as field errors -func ErrDisallowedFields(name string, detail string) FieldErrors { - return FieldErrors{ - field.Forbidden(field.NewPath(name), detail), - } -} - -// Deprecated ErrInvalidArrayValue wraps an invalid error for an array item as field errors -func ErrInvalidArrayValue(value interface{}, name string, index int) FieldErrors { - return FieldErrors{ - field.Invalid(field.NewPath(name).Index(index), value, ""), - } -} - -// Deprecated ErrInvalidValue wraps an invalid error as field errors -func ErrInvalidValue(value interface{}, name string) FieldErrors { - return FieldErrors{ - field.Invalid(field.NewPath(name), value, ""), - } -} - -// Deprecated ErrDuplicateValue wraps an duplicate error as field errors -func ErrDuplicateValue(value interface{}, names ...string) FieldErrors { - errs := FieldErrors{} - - for _, name := range names { - errs = append(errs, field.Duplicate(field.NewPath(name), value)) - } - - return errs -} - -// Deprecated ErrMissingField wraps an required error as field errors -func ErrMissingField(name string) FieldErrors { - return FieldErrors{ - field.Required(field.NewPath(name), ""), - } -} - -// Deprecated ErrMissingOneOf wraps an required error for the specified fields as field errors -func ErrMissingOneOf(names ...string) FieldErrors { - return FieldErrors{ - field.Required(field.NewPath(fmt.Sprintf("[%s]", strings.Join(names, ", "))), "expected exactly one, got neither"), - } -} - -// Deprecated ErrMultipleOneOf wraps an required error for the specified fields as field errors -func ErrMultipleOneOf(names ...string) FieldErrors { - return FieldErrors{ - field.Required(field.NewPath(fmt.Sprintf("[%s]", strings.Join(names, ", "))), "expected exactly one, got both"), - } -} diff --git a/validation/fielderrors_test.go b/validation/fielderrors_test.go deleted file mode 100644 index 6b3f506..0000000 --- a/validation/fielderrors_test.go +++ /dev/null @@ -1,231 +0,0 @@ -/* -Copyright 2019 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -package validation_test - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "k8s.io/apimachinery/pkg/util/validation/field" - - "github.com/vmware-labs/reconciler-runtime/validation" -) - -func TestFieldErrors_Also(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{Field: "field1"}, - &field.Error{Field: "field2"}, - &field.Error{Field: "field3"}, - } - actual := validation.FieldErrors{}.Also( - validation.FieldErrors{ - &field.Error{Field: "field1"}, - &field.Error{Field: "field2"}, - }, - validation.FieldErrors{ - &field.Error{Field: "field3"}, - }, - ) - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestFieldErrors_ViaField(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{Field: "parent"}, - &field.Error{Field: "parent.field"}, - &field.Error{Field: "parent[0]"}, - } - actual := validation.FieldErrors{ - &field.Error{Field: "[]"}, - &field.Error{Field: "field"}, - &field.Error{Field: "[0]"}, - }.ViaField("parent") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestFieldErrors_ViaIndex(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{Field: "[2]"}, - &field.Error{Field: "[2].field"}, - &field.Error{Field: "[2][0]"}, - } - actual := validation.FieldErrors{ - &field.Error{Field: "[]"}, - &field.Error{Field: "field"}, - &field.Error{Field: "[0]"}, - }.ViaIndex(2) - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestFieldErrors_ViaFieldIndex(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{Field: "parent[2]"}, - &field.Error{Field: "parent[2].field"}, - &field.Error{Field: "parent[2][0]"}, - } - actual := validation.FieldErrors{ - &field.Error{Field: "[]"}, - &field.Error{Field: "field"}, - &field.Error{Field: "[0]"}, - }.ViaFieldIndex("parent", 2) - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestFieldErrors_ErrorList(t *testing.T) { - expected := field.ErrorList{ - &field.Error{Field: "[]"}, - } - actual := validation.FieldErrors{ - &field.Error{Field: "[]"}, - }.ErrorList() - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestFieldErrors_ToAggregate(t *testing.T) { - expected := field.ErrorList{ - &field.Error{Field: "[]"}, - }.ToAggregate() - actual := validation.FieldErrors{ - &field.Error{Field: "[]"}, - }.ToAggregate() - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrDisallowedFields(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeForbidden, - Field: "my-field", - BadValue: "", - Detail: "my-detail", - }, - } - actual := validation.ErrDisallowedFields("my-field", "my-detail") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrInvalidArrayValue(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "my-field[1]", - BadValue: "value", - Detail: "", - }, - } - actual := validation.ErrInvalidArrayValue("value", "my-field", 1) - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrInvalidValue(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeInvalid, - Field: "my-field", - BadValue: "value", - Detail: "", - }, - } - actual := validation.ErrInvalidValue("value", "my-field") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrDuplicateValue(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeDuplicate, - Field: "my-field1", - BadValue: "value", - Detail: "", - }, - &field.Error{ - Type: field.ErrorTypeDuplicate, - Field: "my-field2", - BadValue: "value", - Detail: "", - }, - } - actual := validation.ErrDuplicateValue("value", "my-field1", "my-field2") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrMissingField(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "my-field", - BadValue: "", - Detail: "", - }, - } - actual := validation.ErrMissingField("my-field") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrMissingOneOf(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "[field1, field2, field3]", - BadValue: "", - Detail: "expected exactly one, got neither", - }, - } - actual := validation.ErrMissingOneOf("field1", "field2", "field3") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -} - -func TestErrMultipleOneOf(t *testing.T) { - expected := validation.FieldErrors{ - &field.Error{ - Type: field.ErrorTypeRequired, - Field: "[field1, field2, field3]", - BadValue: "", - Detail: "expected exactly one, got both", - }, - } - actual := validation.ErrMultipleOneOf("field1", "field2", "field3") - - if diff := cmp.Diff(expected, actual); diff != "" { - t.Errorf("(-expected, +actual): %s", diff) - } -}