Skip to content
Draft
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
1 change: 1 addition & 0 deletions config/crds/migration.kcp.io_logicalclustermigrations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ spec:
- Migrating
- OriginCleanup
- DestinationFinalize
- Aborting
- Completed
- Failed
type: string
Expand Down
2 changes: 1 addition & 1 deletion config/root-phase0/apiexport-migration.kcp.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ spec:
crd: {}
- group: migration.kcp.io
name: logicalclustermigrations
schema: v260604-70219e163.logicalclustermigrations.migration.kcp.io
schema: v260624-65ebd3edd.logicalclustermigrations.migration.kcp.io
storage:
crd: {}
status: {}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: apis.kcp.io/v1alpha1
kind: APIResourceSchema
metadata:
name: v260604-70219e163.logicalclustermigrations.migration.kcp.io
name: v260624-65ebd3edd.logicalclustermigrations.migration.kcp.io
spec:
group: migration.kcp.io
names:
Expand Down Expand Up @@ -127,6 +127,7 @@ spec:
- Migrating
- OriginCleanup
- DestinationFinalize
- Aborting
- Completed
- Failed
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ func (c *Controller) process(ctx context.Context, key string) error {
return nil
}

// If the LC is currently being migrated, skip reconciliation. The
// migration controller handles etcd cleanup directly for migrating
// clusters via its Aborting phase.
if logicalCluster.Annotations["internal.kcp.io/migrating"] != "" {
logger.V(4).Info("logical cluster is migrating, skipping deletion reconciliation")
return nil
}

logicalClusterCopy := logicalCluster.DeepCopy()

logger.V(2).Info("deleting logical cluster")
Expand Down
46 changes: 46 additions & 0 deletions pkg/reconciler/migration/logicalclustermigration/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
clientv3 "go.etcd.io/etcd/client/v3"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
Expand Down Expand Up @@ -84,6 +85,7 @@ func NewController(
etcdStoragePrefix: etcdStoragePrefix,
logicalClusterLister: localLogicalClusterInformer.Lister(),
shardLister: cachedShardInformer.Lister(),
migrationIndexer: cachedLogicalClusterMigrationInformer.Informer().GetIndexer(),
migratingLogicalClusters: migratingLogicalClusters,
cancelLogicalClusterConnections: cancelLogicalClusterConnections,
ddsif: ddsif,
Expand All @@ -102,6 +104,14 @@ func NewController(
DeleteFunc: func(obj interface{}) { c.enqueue(obj) },
})

// Watch local LogicalClusters for deletion during migration.
// When a workspace is deleted, the LC gets a DeletionTimestamp;
// we need to enqueue the corresponding migration to trigger abort.
_, _ = localLogicalClusterInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: func(_, obj interface{}) { c.enqueueFromLogicalCluster(obj) },
DeleteFunc: func(obj interface{}) { c.enqueueFromLogicalCluster(obj) },
})

return c, nil
}

Expand All @@ -119,6 +129,7 @@ type Controller struct {

logicalClusterLister corev1alpha1listers.LogicalClusterClusterLister
shardLister corev1alpha1listers.ShardClusterLister
migrationIndexer cache.Indexer

migratingLogicalClusters *MigratingLogicalClusters
cancelLogicalClusterConnections func(logicalcluster.Path, error)
Expand All @@ -138,6 +149,41 @@ func (c *Controller) enqueue(obj interface{}) {
c.queue.Add(key)
}

// enqueueFromLogicalCluster handles events from the local LogicalCluster
// informer. If the LC is currently migrating, it enqueues the corresponding
// LogicalClusterMigration so the reconciler can detect deletion and abort.
func (c *Controller) enqueueFromLogicalCluster(obj interface{}) {
metaObj, err := meta.Accessor(obj)
if err != nil {
utilruntime.HandleError(err)
return
}

lcName := logicalcluster.From(metaObj)
if !c.migratingLogicalClusters.IsMigrating(lcName) {
return
}

// Find the migration that references this LC by scanning the cached informer store.
for _, item := range c.migrationIndexer.List() {
migration, ok := item.(*migrationv1alpha1.LogicalClusterMigration)
if !ok {
continue
}
if migration.Spec.LogicalCluster == string(lcName) {
key, err := kcpcache.MetaClusterNamespaceKeyFunc(migration)
if err != nil {
utilruntime.HandleError(err)
continue
}
logger := logging.WithQueueKey(logging.WithReconciler(klog.Background(), ControllerName), key)
logger.V(4).Info("queueing LogicalClusterMigration from LogicalCluster event", "logicalCluster", lcName)
c.queue.Add(key)
return
}
}
}

func (c *Controller) Start(ctx context.Context, numThreads int) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
Expand Down
89 changes: 87 additions & 2 deletions pkg/reconciler/migration/logicalclustermigration/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"

Expand All @@ -42,6 +43,32 @@ func (c *Controller) reconcile(ctx context.Context, migration *migrationv1alpha1
return false, nil
}

// Detect LC deletion during migration. If the origin shard sees the LC
// has a DeletionTimestamp and we're not already in a terminal or aborting
// phase, transition to Aborting to clean up data on both shards.
if isOrigin && !isTerminalOrAborting(migration.Status.Phase) {
lcName := logicalcluster.Name(migration.Spec.LogicalCluster)
lc, err := c.logicalClusterLister.Cluster(lcName).Get(corev1alpha1.LogicalClusterName)
if err == nil && !lc.DeletionTimestamp.IsZero() {
logger.V(2).Info("LogicalCluster is being deleted, transitioning to Aborting", "logicalCluster", lcName)
migration.Status.Phase = migrationv1alpha1.LogicalClusterMigrationPhaseAborting
return true, nil
}
// If the LC is not found in the lister, it may have already been
// fully deleted. Transition to Aborting to ensure cleanup.
if apierrors.IsNotFound(err) {
logger.V(2).Info("LogicalCluster not found in lister, transitioning to Aborting", "logicalCluster", lcName)
migration.Status.Phase = migrationv1alpha1.LogicalClusterMigrationPhaseAborting
return true, nil
}
}

// Requeue on the origin shard during phases where the destination is
// active. This ensures the deletion detection above re-runs even if
// the LC informer event is missed.
requeueForDeletionDetection := isOrigin && (migration.Status.Phase == migrationv1alpha1.LogicalClusterMigrationPhaseMigrating ||
migration.Status.Phase == migrationv1alpha1.LogicalClusterMigrationPhaseDestinationFinalize)

// Route through the process. The overview of the process id
// described in the doc.go, details are in the methods.
switch migration.Status.Phase {
Expand All @@ -65,7 +92,7 @@ func (c *Controller) reconcile(ctx context.Context, migration *migrationv1alpha1
if isDestination {
return c.reconcileMigrating(ctx, migration)
}
return false, nil
return requeueForDeletionDetection, nil

case migrationv1alpha1.LogicalClusterMigrationPhaseOriginCleanup:
if isOrigin {
Expand All @@ -77,7 +104,10 @@ func (c *Controller) reconcile(ctx context.Context, migration *migrationv1alpha1
if isDestination {
return c.reconcileDestinationFinalize(ctx, migration)
}
return false, nil
return requeueForDeletionDetection, nil

case migrationv1alpha1.LogicalClusterMigrationPhaseAborting:
return c.reconcileAborting(ctx, migration)

default:
logger.V(2).Info("unknown phase", "phase", migration.Status.Phase)
Expand Down Expand Up @@ -275,3 +305,58 @@ func (c *Controller) reconcileDestinationFinalize(ctx context.Context, migration
logger.V(2).Info("migration completed", "logicalCluster", lcName)
return false, nil
}

// reconcileAborting handles the Aborting phase, which is entered when the
// LogicalCluster is being deleted during an active migration. Both origin and
// destination shards clean up their local etcd data independently.
func (c *Controller) reconcileAborting(ctx context.Context, migration *migrationv1alpha1.LogicalClusterMigration) (bool, error) {
logger := klog.FromContext(ctx)
lcName := logicalcluster.Name(migration.Spec.LogicalCluster)

logger.V(2).Info("aborting migration due to LogicalCluster deletion", "logicalCluster", lcName)

// Each shard cleans up its own local etcd data.
// deleteOriginData() is idempotent — if no data exists locally, it's a no-op.
if c.isOrigin(migration) || c.isDestination(migration) {
if err := c.deleteOriginData(ctx, lcName); err != nil {
return true, err
}
c.migratingLogicalClusters.Remove(lcName)
}

// Track per-shard completion via conditions.
if c.isOrigin(migration) {
conditions.MarkTrue(migration, migrationv1alpha1.LCMigrationOriginCleaned)
}
if c.isDestination(migration) {
conditions.MarkTrue(migration, migrationv1alpha1.LCMigrationDestinationCleaned)
}

// Both shards done? Transition to terminal Failed state.
// If data was never copied to the destination (DataCopied is not True),
// there's nothing to clean on the destination — only origin cleanup is needed.
originDone := conditions.IsTrue(migration, migrationv1alpha1.LCMigrationOriginCleaned)
destinationDone := conditions.IsTrue(migration, migrationv1alpha1.LCMigrationDestinationCleaned)
destinationNeverHadData := !conditions.IsTrue(migration, migrationv1alpha1.LCMigrationDataCopied)

if originDone && (destinationDone || destinationNeverHadData) {
migration.Status.Phase = migrationv1alpha1.LogicalClusterMigrationPhaseFailed
conditions.MarkTrue(migration, migrationv1alpha1.LCMigrationAborted)
logger.V(2).Info("abort complete, transitioning to Failed", "logicalCluster", lcName)
}

return false, nil
}

// isTerminalOrAborting returns true if the migration is in a terminal state
// or already aborting.
func isTerminalOrAborting(phase migrationv1alpha1.LogicalClusterMigrationPhaseType) bool {
switch phase {
case migrationv1alpha1.LogicalClusterMigrationPhaseCompleted,
migrationv1alpha1.LogicalClusterMigrationPhaseFailed,
migrationv1alpha1.LogicalClusterMigrationPhaseAborting:
return true
default:
return false
}
}
11 changes: 7 additions & 4 deletions pkg/server/filters/migratinglogicalcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ func WithBlockMigratingLogicalClusters(handler http.Handler, isMigrating func(lo
}

userInfo, ok := request.UserFrom(req.Context())
if ok && slices.Contains(userInfo.GetGroups(), bootstrappolicy.SystemExternalLogicalClusterAdmin) {
// allow system:kcp:external-logical-cluster-admin to pass,
// required for the destination shard to get
// logicalclusterdump from the migrating workspace
if ok && (slices.Contains(userInfo.GetGroups(), bootstrappolicy.SystemExternalLogicalClusterAdmin) ||
slices.Contains(userInfo.GetGroups(), bootstrappolicy.SystemMastersGroup)) {
// allow system:kcp:external-logical-cluster-admin and
// system:masters to pass, required for the destination shard
// to get logicalclusterdump from the migrating workspace and
// for internal controllers (e.g. workspace deletion) to manage
// the LogicalCluster during migration.
handler.ServeHTTP(w, req)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ type LogicalClusterMigrationSpec struct {

// LogicalClusterMigrationPhaseType is the type of the current phase of the migration.
//
// +kubebuilder:validation:Enum=Preparing;Migrating;OriginCleanup;DestinationFinalize;Completed;Failed
// +kubebuilder:validation:Enum=Preparing;Migrating;OriginCleanup;DestinationFinalize;Aborting;Completed;Failed
type LogicalClusterMigrationPhaseType string

const (
LogicalClusterMigrationPhasePreparing LogicalClusterMigrationPhaseType = "Preparing"
LogicalClusterMigrationPhaseMigrating LogicalClusterMigrationPhaseType = "Migrating"
LogicalClusterMigrationPhaseOriginCleanup LogicalClusterMigrationPhaseType = "OriginCleanup"
LogicalClusterMigrationPhaseDestinationFinalize LogicalClusterMigrationPhaseType = "DestinationFinalize"
LogicalClusterMigrationPhaseAborting LogicalClusterMigrationPhaseType = "Aborting"
LogicalClusterMigrationPhaseCompleted LogicalClusterMigrationPhaseType = "Completed"
LogicalClusterMigrationPhaseFailed LogicalClusterMigrationPhaseType = "Failed"
)
Expand Down Expand Up @@ -114,9 +115,17 @@ const (
// belonging to the migration logical cluster.
LCMigrationOriginCleaned conditionsv1alpha1.ConditionType = "OriginCleaned"

// LCMigrationDestinationCleaned indicates the destination shard has cleaned up
// all copied data during an abort.
LCMigrationDestinationCleaned conditionsv1alpha1.ConditionType = "DestinationCleaned"

// LCMigrationCompleted indicates the migration has fully completed and the
// logical cluster is available on the destination shard.
LCMigrationCompleted conditionsv1alpha1.ConditionType = "Completed"

// LCMigrationAborted indicates the migration was aborted (e.g. due to
// workspace deletion) and both shards have cleaned up their data.
LCMigrationAborted conditionsv1alpha1.ConditionType = "Aborted"
)

func (in *LogicalClusterMigration) SetConditions(c conditionsv1alpha1.Conditions) {
Expand Down
Loading