Problem
When two or more InitTarget resources reference the same WorkspaceType, the init-agent creates separate initcontroller instances for each. Because InitializerForType() derives the initializer name deterministically from the WorkspaceType (not the InitTarget), all controllers share the same initializer name but operate independently.
This causes a race condition: the first controller to finish applying its sources removes the initializer from the LogicalCluster.Status.Initializers list, which causes the second controller to skip its sources entirely (reconciler.go line 64: if !slices.Contains(lc.Status.Initializers, r.initializer) { return }).
Reproduction
- Create two InitTargets in the config workspace, both referencing the same WorkspaceType:
apiVersion: initialization.kcp.io/v1alpha1
kind: InitTarget
metadata:
name: init-rbac
spec:
workspaceTypeRef:
path: root
name: account
sources:
- template:
name: rbac-template
---
apiVersion: initialization.kcp.io/v1alpha1
kind: InitTarget
metadata:
name: init-quota
spec:
workspaceTypeRef:
path: root
name: account
sources:
- template:
name: quota-template
- Create a workspace of type
account.
- Observe that only one InitTarget's sources are applied; the other is silently skipped.
Root Cause
In targetcontroller/controller.go, getInitTargetKey() returns target.UID, so each InitTarget passes the "controller already exists" check independently (line 129). Each gets its own multicluster manager and initcontroller, but all use the same initializer name from InitializerForType(wst).
The initcontroller in reconciler.go treats the initializer as exclusively owned: once any controller removes it, the others see the initializer is gone and bail out at line 64 without applying their templates.
Impact
- Templates from one or more InitTargets are silently never applied to new workspaces.
- The behavior is non-deterministic (depends on which controller wins the race).
- Users splitting initialization into multiple InitTargets for modularity (e.g., RBAC + quotas + networking) will experience incomplete workspace initialization with no error signal.
Proposed Fix
Change the targetcontroller to key its controllers by WorkspaceType reference (path:name) instead of InitTarget UID. When multiple InitTargets reference the same WorkspaceType:
-
Single controller per WorkspaceType: Only one multicluster manager and initcontroller is created for each unique WorkspaceType, regardless of how many InitTargets reference it.
-
Aggregate sources: The InitTargetProvider returns all InitTargets for the WorkspaceType. The initcontroller iterates sources from all of them before considering initialization complete.
-
Lifecycle management: The controller is only stopped when the last InitTarget for a given WorkspaceType is deleted.
Files affected:
internal/controller/targetcontroller/controller.go — key by WSType, aggregate target tracking
internal/controller/initcontroller/controller.go — InitTargetProvider returns []*InitTarget
internal/controller/initcontroller/reconciler.go — iterate all targets' sources, remove initializer only when all complete
Problem
When two or more
InitTargetresources reference the sameWorkspaceType, the init-agent creates separate initcontroller instances for each. BecauseInitializerForType()derives the initializer name deterministically from the WorkspaceType (not the InitTarget), all controllers share the same initializer name but operate independently.This causes a race condition: the first controller to finish applying its sources removes the initializer from the
LogicalCluster.Status.Initializerslist, which causes the second controller to skip its sources entirely (reconciler.goline 64:if !slices.Contains(lc.Status.Initializers, r.initializer) { return }).Reproduction
account.Root Cause
In
targetcontroller/controller.go,getInitTargetKey()returnstarget.UID, so each InitTarget passes the "controller already exists" check independently (line 129). Each gets its own multicluster manager and initcontroller, but all use the same initializer name fromInitializerForType(wst).The initcontroller in
reconciler.gotreats the initializer as exclusively owned: once any controller removes it, the others see the initializer is gone and bail out at line 64 without applying their templates.Impact
Proposed Fix
Change the targetcontroller to key its controllers by WorkspaceType reference (
path:name) instead of InitTarget UID. When multiple InitTargets reference the same WorkspaceType:Single controller per WorkspaceType: Only one multicluster manager and initcontroller is created for each unique WorkspaceType, regardless of how many InitTargets reference it.
Aggregate sources: The
InitTargetProviderreturns all InitTargets for the WorkspaceType. The initcontroller iterates sources from all of them before considering initialization complete.Lifecycle management: The controller is only stopped when the last InitTarget for a given WorkspaceType is deleted.
Files affected:
internal/controller/targetcontroller/controller.go— key by WSType, aggregate target trackinginternal/controller/initcontroller/controller.go—InitTargetProviderreturns[]*InitTargetinternal/controller/initcontroller/reconciler.go— iterate all targets' sources, remove initializer only when all complete