diff --git a/lib/services/reconciler.go b/lib/services/reconciler.go index 17b136a056152..9aa23cac0007f 100644 --- a/lib/services/reconciler.go +++ b/lib/services/reconciler.go @@ -55,6 +55,11 @@ type GenericReconcilerConfig[K comparable, T any] struct { OnDelete func(context.Context, T) error // Logger emits log messages. Logger *slog.Logger + // AllowOriginChanges is a flag that allows the reconciler to change the + // origin value of a reconciled resource. By default, origin changes are + // disallowed to enforce segregation between of resources from different + // sources. + AllowOriginChanges bool } // CheckAndSetDefaults validates the reconciler configuration and sets defaults. @@ -177,18 +182,21 @@ func (r *GenericReconciler[K, T]) processNewResource(ctx context.Context, curren return nil } - // Don't overwrite resource of a different origin (e.g., keep static resource from config and ignore dynamic resource) - registeredOrigin, err := types.GetOrigin(registered) - if err != nil { - return trace.Wrap(err) - } - newOrigin, err := types.GetOrigin(newT) - if err != nil { - return trace.Wrap(err) - } - if registeredOrigin != newOrigin { - r.logger.WarnContext(ctx, "New resource has different origin, not updating", "name", key, "new_origin", newOrigin, "existing_origin", registeredOrigin) - return nil + if !r.cfg.AllowOriginChanges { + // Don't overwrite resource of a different origin (e.g., keep static resource from config and ignore dynamic resource) + registeredOrigin, err := types.GetOrigin(registered) + if err != nil { + return trace.Wrap(err) + } + newOrigin, err := types.GetOrigin(newT) + if err != nil { + return trace.Wrap(err) + } + if registeredOrigin != newOrigin { + r.logger.WarnContext(ctx, "New resource has different origin, not updating", + "name", key, "new_origin", newOrigin, "existing_origin", registeredOrigin) + return nil + } } // If the resource is already registered but was updated, see if its diff --git a/lib/services/reconciler_test.go b/lib/services/reconciler_test.go index 37ca13e1447a0..ad97fb61c904d 100644 --- a/lib/services/reconciler_test.go +++ b/lib/services/reconciler_test.go @@ -43,6 +43,7 @@ func TestReconciler(t *testing.T) { onCreateCalls []testResource onUpdateCalls []updateCall onDeleteCalls []testResource + configure func(cfg *ReconcilerConfig[testResource]) comparator func(testResource, testResource) int }{ { @@ -73,13 +74,30 @@ func TestReconciler(t *testing.T) { }, }, { - description: "resources with different origins don't overwrite each other", + description: "resources with different origins don't overwrite each other by default", selectors: []ResourceMatcher{{ Labels: types.Labels{"*": []string{"*"}}, }}, registeredResources: []testResource{makeStaticResource("res1", nil)}, newResources: []testResource{makeDynamicResource("res1", nil)}, }, + { + description: "resources with different origins overwrite each other when allowed", + selectors: []ResourceMatcher{{ + Labels: types.Labels{"*": []string{"*"}}, + }}, + configure: func(cfg *ReconcilerConfig[testResource]) { + cfg.AllowOriginChanges = true + }, + registeredResources: []testResource{makeStaticResource("res1", nil)}, + newResources: []testResource{makeDynamicResource("res1", nil)}, + onUpdateCalls: []updateCall{ + { + old: makeStaticResource("res1", nil), + new: makeDynamicResource("res1", nil), + }, + }, + }, { description: "resource that's no longer present should be removed", selectors: []ResourceMatcher{{ @@ -198,7 +216,7 @@ func TestReconciler(t *testing.T) { var onCreateCalls, onDeleteCalls []testResource var onUpdateCalls []updateCall - reconciler, err := NewReconciler[testResource](ReconcilerConfig[testResource]{ + cfg := ReconcilerConfig[testResource]{ Matcher: func(tr testResource) bool { return MatchResourceLabels(test.selectors, tr.GetMetadata().Labels) }, @@ -225,7 +243,13 @@ func TestReconciler(t *testing.T) { onDeleteCalls = append(onDeleteCalls, tr) return nil }, - }) + } + + if test.configure != nil { + test.configure(&cfg) + } + + reconciler, err := NewReconciler[testResource](cfg) require.NoError(t, err) // Reconcile and make sure we got all expected callback calls.