Skip to content

Commit 6ad35e2

Browse files
authored
feat: support multiple InitTargets for the same WorkspaceType (#20)
Previously, init-agent documentation stated that only a single InitTarget per WorkspaceType was supported — a second InitTarget referencing the same WorkspaceType would race the first and one set of templates could be silently skipped. This change makes multiple InitTargets per WorkspaceType a first-class feature. The targetcontroller now keys controllers by WorkspaceType reference (path:name) instead of InitTarget UID. Sources from all matching InitTargets are aggregated into a single initialization pass, and the initializer is only removed after every source from every target is applied. - internal/controller/targetcontroller: key by WSType ref, track InitTarget names per key, aggregate provider returns all matching targets, cancel manager only when the last target is deleted. - internal/controller/initcontroller: InitTargetsProvider returns []*InitTarget; reconcile iterates sources from all of them. - Rename InitTargetProvider -> InitTargetsProvider (plural) per review feedback from @xrstf. Fixes #17 Signed-off-by: Igor Fominykh <ifdotpy@gmail.com>
1 parent 50421ac commit 6ad35e2

File tree

5 files changed

+96
-52
lines changed

5 files changed

+96
-52
lines changed

cmd/init-agent/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {
119119

120120
// wrap this controller creation in a closure to prevent giving all the initcontroller
121121
// dependencies to the targetcontroller
122-
newInitController := func(remoteManager mcmanager.Manager, targetProvider initcontroller.InitTargetProvider, initializer kcpcorev1alpha1.LogicalClusterInitializer) error {
122+
newInitController := func(remoteManager mcmanager.Manager, targetProvider initcontroller.InitTargetsProvider, initializer kcpcorev1alpha1.LogicalClusterInitializer) error {
123123
return initcontroller.Create(remoteManager, targetProvider, sourceFactory, manifestApplier, initializer, log, numInitWorkers)
124124
}
125125

docs/content/setup/development.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ that backs a workspace. Initializers are the cluster name + name of the `Workspa
2121
removed once from a `LogicalCluster`, it's critical that you use dedicated workspace types for
2222
every bootstrapping purpose.
2323

24-
This means there can only be exactly one `InitTarget` in the entire kcp installation that refers
25-
to a `WorkspaceType`. And only a single init-agent may process each `InitTarget`.
24+
Multiple `InitTarget` resources may refer to the same `WorkspaceType`. The init-agent aggregates
25+
the sources from all of them into a single initialization pass and only removes the initializer
26+
after every source from every target has been applied. This lets you compose bootstrapping
27+
behavior from independently authored `InitTargets` (e.g. RBAC, quotas, networking) without
28+
racing on the initializer. Only a single init-agent may process a given `WorkspaceType`.
2629

2730
**Do not** use the init-agent with kcp's own `WorkspaceTypes`, as this could interfere with
2831
kcp's core functionality.

internal/controller/initcontroller/controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ const (
3838
ControllerName = "initagent-init"
3939
)
4040

41-
type InitTargetProvider func(ctx context.Context) (*initializationv1alpha1.InitTarget, error)
41+
type InitTargetsProvider func(ctx context.Context) ([]*initializationv1alpha1.InitTarget, error)
4242

4343
type Reconciler struct {
4444
remoteManager mcmanager.Manager
45-
targetProvider InitTargetProvider
45+
targetProvider InitTargetsProvider
4646
log *zap.SugaredLogger
4747
sourceFactory *source.Factory
4848
manifestApplier manifest.Applier
@@ -53,7 +53,7 @@ type Reconciler struct {
5353
// as this controller is started/stopped by the syncmanager controller instead.
5454
func Create(
5555
remoteManager mcmanager.Manager,
56-
targetProvider InitTargetProvider,
56+
targetProvider InitTargetsProvider,
5757
sourceFactory *source.Factory,
5858
manifestApplier manifest.Applier,
5959
initializer kcpcorev1alpha1.LogicalClusterInitializer,

internal/controller/initcontroller/reconciler.go

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -89,38 +89,40 @@ func (r *Reconciler) Reconcile(ctx context.Context, request mcreconcile.Request)
8989
}
9090

9191
func (r *Reconciler) reconcile(ctx context.Context, logger *zap.SugaredLogger, client ctrlruntimeclient.Client, lc *kcpcorev1alpha1.LogicalCluster) (requeue bool, err error) {
92-
// Dynamically fetch the latest InitTarget, so that we do not have to restart
93-
// (and re-cache) this controller everytime an InitTarget changes.
94-
target, err := r.targetProvider(ctx)
92+
// Dynamically fetch all InitTargets for this WorkspaceType, so that we do not
93+
// have to restart (and re-cache) this controller everytime an InitTarget changes.
94+
targets, err := r.targetProvider(ctx)
9595
if err != nil {
96-
return requeue, fmt.Errorf("failed to get InitTarget: %w", err)
96+
return requeue, fmt.Errorf("failed to get InitTargets: %w", err)
9797
}
9898

99-
for idx, ref := range target.Spec.Sources {
100-
sourceLog := logger.With("init-target", target.Name, "source-idx", idx)
101-
sourceCtx := log.WithLog(ctx, sourceLog)
102-
103-
src, err := r.sourceFactory.NewForInitSource(sourceCtx, kcp.ClusterNameFromObject(target), ref)
104-
if err != nil {
105-
return requeue, fmt.Errorf("failed to initialize source #%d: %w", idx, err)
106-
}
107-
108-
objects, err := src.Manifests(lc)
109-
if err != nil {
110-
return requeue, fmt.Errorf("failed to render source #%d: %w", idx, err)
111-
}
112-
113-
sourceLog.Debugf("Source yielded %d manifests", len(objects))
114-
115-
srcNeedRequeue, err := r.manifestApplier.Apply(sourceCtx, client, objects)
116-
if err != nil {
117-
return requeue, fmt.Errorf("failed to apply source #%d: %w", idx, err)
118-
}
119-
120-
// If one source cannot be completed at this time, continue with the others.
121-
if srcNeedRequeue {
122-
sourceLog.Debug("Source requires requeuing")
123-
requeue = true
99+
for _, target := range targets {
100+
for idx, ref := range target.Spec.Sources {
101+
sourceLog := logger.With("init-target", target.Name, "source-idx", idx)
102+
sourceCtx := log.WithLog(ctx, sourceLog)
103+
104+
src, err := r.sourceFactory.NewForInitSource(sourceCtx, kcp.ClusterNameFromObject(target), ref)
105+
if err != nil {
106+
return requeue, fmt.Errorf("failed to initialize source #%d from target %s: %w", idx, target.Name, err)
107+
}
108+
109+
objects, err := src.Manifests(lc)
110+
if err != nil {
111+
return requeue, fmt.Errorf("failed to render source #%d from target %s: %w", idx, target.Name, err)
112+
}
113+
114+
sourceLog.Debugf("Source yielded %d manifests", len(objects))
115+
116+
srcNeedRequeue, err := r.manifestApplier.Apply(sourceCtx, client, objects)
117+
if err != nil {
118+
return requeue, fmt.Errorf("failed to apply source #%d from target %s: %w", idx, target.Name, err)
119+
}
120+
121+
// If one source cannot be completed at this time, continue with the others.
122+
if srcNeedRequeue {
123+
sourceLog.Debug("Source requires requeuing")
124+
requeue = true
125+
}
124126
}
125127
}
126128

internal/controller/targetcontroller/controller.go

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const (
5050
ControllerName = "initagent-target-controller"
5151
)
5252

53-
type NewInitControllerFunc func(remoteManager mcmanager.Manager, targetProvider initcontroller.InitTargetProvider, initializer kcpcorev1alpha1.LogicalClusterInitializer) error
53+
type NewInitControllerFunc func(remoteManager mcmanager.Manager, targetProvider initcontroller.InitTargetsProvider, initializer kcpcorev1alpha1.LogicalClusterInitializer) error
5454

5555
type Reconciler struct {
5656
// Choose to break good practice of never storing a context in a struct,
@@ -65,8 +65,10 @@ type Reconciler struct {
6565
newInitController NewInitControllerFunc
6666

6767
// A map of cancel funcs for the multicluster managers
68-
// that we launch for each InitTarget.
68+
// that we launch for each WorkspaceType.
6969
ctrlCancels map[string]context.CancelCauseFunc
70+
// Tracks which InitTarget names belong to each WorkspaceType key.
71+
ctrlTargets map[string]map[string]bool
7072
ctrlLock sync.Mutex
7173
}
7274

@@ -86,6 +88,7 @@ func Add(
8688
clusterClient: clusterClient,
8789
newInitController: newInitController,
8890
ctrlCancels: map[string]context.CancelCauseFunc{},
91+
ctrlTargets: map[string]map[string]bool{},
8992
ctrlLock: sync.Mutex{},
9093
}
9194

@@ -125,10 +128,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
125128
func (r *Reconciler) ensureInitController(ctx context.Context, log *zap.SugaredLogger, target *initializationv1alpha1.InitTarget) (reconcile.Result, error) {
126129
key := getInitTargetKey(target)
127130

128-
// controller already exists
131+
r.ctrlLock.Lock()
129132
if _, exists := r.ctrlCancels[key]; exists {
133+
// Controller already exists for this WorkspaceType, just track the target.
134+
if r.ctrlTargets[key] == nil {
135+
r.ctrlTargets[key] = map[string]bool{}
136+
}
137+
r.ctrlTargets[key][target.Name] = true
138+
r.ctrlLock.Unlock()
130139
return reconcile.Result{}, nil
131140
}
141+
r.ctrlLock.Unlock()
132142

133143
ctrlog := log.With("ctrlkey", key, "name", target.Name)
134144

@@ -148,15 +158,21 @@ func (r *Reconciler) ensureInitController(ctx context.Context, log *zap.SugaredL
148158
return reconcile.Result{}, fmt.Errorf("failed to create multicluster manager: %w", err)
149159
}
150160

151-
if err := r.newInitController(mgr, r.newInitTargetProvider(target.Name), initializer); err != nil {
161+
if err := r.newInitController(mgr, r.newInitTargetsProvider(key), initializer); err != nil {
152162
return reconcile.Result{}, fmt.Errorf("failed to create init controller: %w", err)
153163
}
154164

155165
// Use the global app context so this provider is independent of the reconcile
156166
// context, which might get cancelled right after Reconcile() is done.
157167
ctrlCtx, ctrlCancel := context.WithCancelCause(r.ctx)
158168

169+
r.ctrlLock.Lock()
159170
r.ctrlCancels[key] = ctrlCancel
171+
if r.ctrlTargets[key] == nil {
172+
r.ctrlTargets[key] = map[string]bool{}
173+
}
174+
r.ctrlTargets[key][target.Name] = true
175+
r.ctrlLock.Unlock()
160176

161177
// cleanup when the context is done
162178
go func() {
@@ -166,6 +182,7 @@ func (r *Reconciler) ensureInitController(ctx context.Context, log *zap.SugaredL
166182
defer r.ctrlLock.Unlock()
167183

168184
delete(r.ctrlCancels, key)
185+
delete(r.ctrlTargets, key)
169186
}()
170187

171188
// time to start the manager
@@ -181,15 +198,22 @@ func (r *Reconciler) ensureInitController(ctx context.Context, log *zap.SugaredL
181198

182199
func (r *Reconciler) cleanupController(log *zap.SugaredLogger, target *initializationv1alpha1.InitTarget) error {
183200
key := getInitTargetKey(target)
184-
log.Infow("Stopping init controller…", "ctrlkey", key)
201+
log.Infow("Removing InitTarget from controller…", "ctrlkey", key, "target", target.Name)
185202

186203
r.ctrlLock.Lock()
187204
defer r.ctrlLock.Unlock()
188205

189-
cancel, ok := r.ctrlCancels[key]
190-
if ok {
191-
cancel(errors.New("controller is no longer needed"))
192-
delete(r.ctrlCancels, key)
206+
if targets, ok := r.ctrlTargets[key]; ok {
207+
delete(targets, target.Name)
208+
if len(targets) == 0 {
209+
// Last target removed, stop the controller.
210+
log.Infow("Stopping init controller (last InitTarget removed)…", "ctrlkey", key)
211+
if cancel, ok := r.ctrlCancels[key]; ok {
212+
cancel(errors.New("controller is no longer needed"))
213+
delete(r.ctrlCancels, key)
214+
}
215+
delete(r.ctrlTargets, key)
216+
}
193217
}
194218

195219
return nil
@@ -250,17 +274,32 @@ func (r *Reconciler) createMulticlusterManager(wst *kcptenancyv1alpha1.Workspace
250274
return mgr, nil
251275
}
252276

253-
func (r *Reconciler) newInitTargetProvider(name string) initcontroller.InitTargetProvider {
254-
return func(ctx context.Context) (*initializationv1alpha1.InitTarget, error) {
255-
target := &initializationv1alpha1.InitTarget{}
256-
if err := r.localClient.Get(ctx, types.NamespacedName{Name: name}, target); err != nil {
257-
return nil, err
277+
func (r *Reconciler) newInitTargetsProvider(wstKey string) initcontroller.InitTargetsProvider {
278+
return func(ctx context.Context) ([]*initializationv1alpha1.InitTarget, error) {
279+
r.ctrlLock.Lock()
280+
targetNames := r.ctrlTargets[wstKey]
281+
names := make([]string, 0, len(targetNames))
282+
for name := range targetNames {
283+
names = append(names, name)
258284
}
259-
260-
return target, nil
285+
r.ctrlLock.Unlock()
286+
287+
var targets []*initializationv1alpha1.InitTarget
288+
for _, name := range names {
289+
target := &initializationv1alpha1.InitTarget{}
290+
if err := r.localClient.Get(ctx, types.NamespacedName{Name: name}, target); err != nil {
291+
if ctrlruntimeclient.IgnoreNotFound(err) == nil {
292+
continue // target was deleted
293+
}
294+
return nil, err
295+
}
296+
targets = append(targets, target)
297+
}
298+
return targets, nil
261299
}
262300
}
263301

264302
func getInitTargetKey(target *initializationv1alpha1.InitTarget) string {
265-
return string(target.UID)
303+
ref := target.Spec.WorkspaceTypeReference
304+
return ref.Path + ":" + ref.Name
266305
}

0 commit comments

Comments
 (0)