Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/init-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error {

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

Expand Down
7 changes: 5 additions & 2 deletions docs/content/setup/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ that backs a workspace. Initializers are the cluster name + name of the `Workspa
removed once from a `LogicalCluster`, it's critical that you use dedicated workspace types for
every bootstrapping purpose.

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

**Do not** use the init-agent with kcp's own `WorkspaceTypes`, as this could interfere with
kcp's core functionality.
Expand Down
6 changes: 3 additions & 3 deletions internal/controller/initcontroller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ const (
ControllerName = "initagent-init"
)

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

type Reconciler struct {
remoteManager mcmanager.Manager
targetProvider InitTargetProvider
targetProvider InitTargetsProvider
log *zap.SugaredLogger
sourceFactory *source.Factory
manifestApplier manifest.Applier
Expand All @@ -53,7 +53,7 @@ type Reconciler struct {
// as this controller is started/stopped by the syncmanager controller instead.
func Create(
remoteManager mcmanager.Manager,
targetProvider InitTargetProvider,
targetProvider InitTargetsProvider,
sourceFactory *source.Factory,
manifestApplier manifest.Applier,
initializer kcpcorev1alpha1.LogicalClusterInitializer,
Expand Down
60 changes: 31 additions & 29 deletions internal/controller/initcontroller/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,38 +89,40 @@ func (r *Reconciler) Reconcile(ctx context.Context, request mcreconcile.Request)
}

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

for idx, ref := range target.Spec.Sources {
sourceLog := logger.With("init-target", target.Name, "source-idx", idx)
sourceCtx := log.WithLog(ctx, sourceLog)

src, err := r.sourceFactory.NewForInitSource(sourceCtx, kcp.ClusterNameFromObject(target), ref)
if err != nil {
return requeue, fmt.Errorf("failed to initialize source #%d: %w", idx, err)
}

objects, err := src.Manifests(lc)
if err != nil {
return requeue, fmt.Errorf("failed to render source #%d: %w", idx, err)
}

sourceLog.Debugf("Source yielded %d manifests", len(objects))

srcNeedRequeue, err := r.manifestApplier.Apply(sourceCtx, client, objects)
if err != nil {
return requeue, fmt.Errorf("failed to apply source #%d: %w", idx, err)
}

// If one source cannot be completed at this time, continue with the others.
if srcNeedRequeue {
sourceLog.Debug("Source requires requeuing")
requeue = true
for _, target := range targets {
for idx, ref := range target.Spec.Sources {
sourceLog := logger.With("init-target", target.Name, "source-idx", idx)
sourceCtx := log.WithLog(ctx, sourceLog)

src, err := r.sourceFactory.NewForInitSource(sourceCtx, kcp.ClusterNameFromObject(target), ref)
if err != nil {
return requeue, fmt.Errorf("failed to initialize source #%d from target %s: %w", idx, target.Name, err)
}

objects, err := src.Manifests(lc)
if err != nil {
return requeue, fmt.Errorf("failed to render source #%d from target %s: %w", idx, target.Name, err)
}

sourceLog.Debugf("Source yielded %d manifests", len(objects))

srcNeedRequeue, err := r.manifestApplier.Apply(sourceCtx, client, objects)
if err != nil {
return requeue, fmt.Errorf("failed to apply source #%d from target %s: %w", idx, target.Name, err)
}

// If one source cannot be completed at this time, continue with the others.
if srcNeedRequeue {
sourceLog.Debug("Source requires requeuing")
requeue = true
}
}
}

Expand Down
73 changes: 56 additions & 17 deletions internal/controller/targetcontroller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const (
ControllerName = "initagent-target-controller"
)

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

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

// A map of cancel funcs for the multicluster managers
// that we launch for each InitTarget.
// that we launch for each WorkspaceType.
ctrlCancels map[string]context.CancelCauseFunc
// Tracks which InitTarget names belong to each WorkspaceType key.
ctrlTargets map[string]map[string]bool
ctrlLock sync.Mutex
}

Expand All @@ -86,6 +88,7 @@ func Add(
clusterClient: clusterClient,
newInitController: newInitController,
ctrlCancels: map[string]context.CancelCauseFunc{},
ctrlTargets: map[string]map[string]bool{},
ctrlLock: sync.Mutex{},
}

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

// controller already exists
r.ctrlLock.Lock()
if _, exists := r.ctrlCancels[key]; exists {
// Controller already exists for this WorkspaceType, just track the target.
if r.ctrlTargets[key] == nil {
r.ctrlTargets[key] = map[string]bool{}
}
r.ctrlTargets[key][target.Name] = true
r.ctrlLock.Unlock()
return reconcile.Result{}, nil
}
r.ctrlLock.Unlock()

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

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

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

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

r.ctrlLock.Lock()
r.ctrlCancels[key] = ctrlCancel
if r.ctrlTargets[key] == nil {
r.ctrlTargets[key] = map[string]bool{}
}
r.ctrlTargets[key][target.Name] = true
r.ctrlLock.Unlock()

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

delete(r.ctrlCancels, key)
delete(r.ctrlTargets, key)
}()

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

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

r.ctrlLock.Lock()
defer r.ctrlLock.Unlock()

cancel, ok := r.ctrlCancels[key]
if ok {
cancel(errors.New("controller is no longer needed"))
delete(r.ctrlCancels, key)
if targets, ok := r.ctrlTargets[key]; ok {
delete(targets, target.Name)
if len(targets) == 0 {
// Last target removed, stop the controller.
log.Infow("Stopping init controller (last InitTarget removed)…", "ctrlkey", key)
if cancel, ok := r.ctrlCancels[key]; ok {
cancel(errors.New("controller is no longer needed"))
delete(r.ctrlCancels, key)
}
delete(r.ctrlTargets, key)
}
}

return nil
Expand Down Expand Up @@ -250,17 +274,32 @@ func (r *Reconciler) createMulticlusterManager(wst *kcptenancyv1alpha1.Workspace
return mgr, nil
}

func (r *Reconciler) newInitTargetProvider(name string) initcontroller.InitTargetProvider {
return func(ctx context.Context) (*initializationv1alpha1.InitTarget, error) {
target := &initializationv1alpha1.InitTarget{}
if err := r.localClient.Get(ctx, types.NamespacedName{Name: name}, target); err != nil {
return nil, err
func (r *Reconciler) newInitTargetsProvider(wstKey string) initcontroller.InitTargetsProvider {
return func(ctx context.Context) ([]*initializationv1alpha1.InitTarget, error) {
r.ctrlLock.Lock()
targetNames := r.ctrlTargets[wstKey]
names := make([]string, 0, len(targetNames))
for name := range targetNames {
names = append(names, name)
}

return target, nil
r.ctrlLock.Unlock()

var targets []*initializationv1alpha1.InitTarget
for _, name := range names {
target := &initializationv1alpha1.InitTarget{}
if err := r.localClient.Get(ctx, types.NamespacedName{Name: name}, target); err != nil {
if ctrlruntimeclient.IgnoreNotFound(err) == nil {
continue // target was deleted
}
return nil, err
}
targets = append(targets, target)
}
return targets, nil
}
}

func getInitTargetKey(target *initializationv1alpha1.InitTarget) string {
return string(target.UID)
ref := target.Spec.WorkspaceTypeReference
return ref.Path + ":" + ref.Name
}
Loading