Skip to content

Commit 7992763

Browse files
committed
refactor(dataprotection): clarify restore lifecycle coordination
1 parent ca8a901 commit 7992763

5 files changed

Lines changed: 39 additions & 46 deletions

File tree

controllers/dataprotection/cluster_restore_controller.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ import (
4141
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
4242
)
4343

44-
// ClusterRestoreReconciler owns only the Cluster restore-protection finalizer.
45-
// VolumePopulator remains the owner of all PVC-scoped restore resources.
44+
// ClusterRestoreReconciler coordinates the Cluster-level restore lifecycle.
4645
type ClusterRestoreReconciler struct {
4746
client.Client
4847
Recorder record.EventRecorder
@@ -64,20 +63,20 @@ func (r *ClusterRestoreReconciler) Reconcile(ctx context.Context, req ctrl.Reque
6463
return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")
6564
}
6665

67-
active, err := r.hasRestoreResources(ctx, r.Client, cluster)
66+
hasResources, err := r.hasRestoreResources(ctx, cluster)
6867
if err != nil {
6968
return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "failed to inspect Cluster restore resources")
7069
}
7170
if cluster.DeletionTimestamp.IsZero() {
72-
if clusterRestoreConditionActive(cluster) || active {
71+
if clusterRestoreConditionActive(cluster) || hasResources {
7372
return r.ensureFinalizer(reqCtx, cluster)
7473
}
7574
return r.removeFinalizer(reqCtx, cluster)
7675
}
7776
if !controllerutil.ContainsFinalizer(cluster, dptypes.RestoreProtectionFinalizerName) {
7877
return intctrlutil.Reconciled()
7978
}
80-
if active {
79+
if hasResources {
8180
return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log,
8281
"waiting for restore resource owners to finish Cluster termination")
8382
}
@@ -113,10 +112,10 @@ func clusterRestoreConditionActive(cluster *appsv1.Cluster) bool {
113112
return condition == nil || condition.Status != metav1.ConditionTrue
114113
}
115114

116-
func (r *ClusterRestoreReconciler) hasRestoreResources(ctx context.Context, reader client.Reader,
115+
func (r *ClusterRestoreReconciler) hasRestoreResources(ctx context.Context,
117116
cluster *appsv1.Cluster) (bool, error) {
118117
restores := &dpv1alpha1.RestoreList{}
119-
if err := reader.List(ctx, restores, client.InNamespace(cluster.Namespace), client.MatchingLabels{
118+
if err := r.Client.List(ctx, restores, client.InNamespace(cluster.Namespace), client.MatchingLabels{
120119
constant.AppInstanceLabelKey: cluster.Name,
121120
}); err != nil {
122121
return false, err
@@ -126,17 +125,17 @@ func (r *ClusterRestoreReconciler) hasRestoreResources(ctx context.Context, read
126125
if restore.Labels[dprestore.DataProtectionRestoreLabelKey] != restore.Name {
127126
continue
128127
}
129-
ownedByCurrentCluster := restore.Labels[dptypes.ClusterUIDLabelKey] == string(cluster.UID)
128+
owned := restore.Labels[dptypes.ClusterUIDLabelKey] == string(cluster.UID)
130129
terminal := restore.Status.Phase == dpv1alpha1.RestorePhaseCompleted ||
131130
restore.Status.Phase == dpv1alpha1.RestorePhaseFailed
132-
if ownedByCurrentCluster && (!cluster.DeletionTimestamp.IsZero() ||
131+
if owned && (!cluster.DeletionTimestamp.IsZero() ||
133132
!terminal || !restore.DeletionTimestamp.IsZero()) {
134133
return true, nil
135134
}
136135
}
137136

138137
pvcs := &corev1.PersistentVolumeClaimList{}
139-
if err := reader.List(ctx, pvcs, client.InNamespace(cluster.Namespace), client.MatchingLabels{
138+
if err := r.Client.List(ctx, pvcs, client.InNamespace(cluster.Namespace), client.MatchingLabels{
140139
constant.AppInstanceLabelKey: cluster.Name,
141140
}); err != nil {
142141
return false, err

controllers/dataprotection/cluster_restore_controller_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import (
3939
dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types"
4040
)
4141

42-
func TestClusterRestoreControllerOwnsOnlyClusterFinalizer(t *testing.T) {
42+
func TestClusterRestoreControllerAddsProtectionForRestoreIntent(t *testing.T) {
4343
scheme := clusterRestoreTestScheme(t)
4444
cluster := activeRestoreCluster()
4545
cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()
@@ -93,7 +93,7 @@ func TestClusterRestoreControllerReleasesFinalizerAfterOwnersFinish(t *testing.T
9393
require.Contains(t, current.Finalizers, "example.io/keep")
9494
}
9595

96-
func TestClusterRestoreControllerTreatsCompletedRestoreAsInactive(t *testing.T) {
96+
func TestClusterRestoreControllerReleasesProtectionAfterSuccessfulRestore(t *testing.T) {
9797
scheme := clusterRestoreTestScheme(t)
9898
cluster := activeRestoreCluster()
9999
cluster.Finalizers = []string{dptypes.RestoreProtectionFinalizerName, "example.io/keep"}
@@ -112,7 +112,7 @@ func TestClusterRestoreControllerTreatsCompletedRestoreAsInactive(t *testing.T)
112112
require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(cluster), current))
113113
require.NotContains(t, current.Finalizers, dptypes.RestoreProtectionFinalizerName)
114114
require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(restore), &dpv1alpha1.Restore{}),
115-
"the Cluster controller observes terminal Restore objects but does not delete them")
115+
"ClusterRestoreReconciler must leave terminal Restore objects for their resource owner to manage")
116116
}
117117

118118
func TestClusterRestoreControllerKeepsProtectionAfterRestoreFailure(t *testing.T) {

controllers/dataprotection/volumepopulator_controller.go

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import (
6060
viper "github.com/apecloud/kubeblocks/pkg/viperx"
6161
)
6262

63-
// VolumePopulatorReconciler reconciles Backup dataSource PVCs.
63+
// VolumePopulatorReconciler coordinates data population and restore for PVCs.
6464
type VolumePopulatorReconciler struct {
6565
client.Client
6666
Scheme *runtime.Scheme
@@ -93,11 +93,7 @@ type pvcRestoreDecision struct {
9393
// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=components,verbs=get;list;watch
9494
// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=componentdefinitions,verbs=get;list;watch
9595

96-
// Reconcile is part of the main kubernetes reconciliation loop which aims to
97-
// move the current state of the cluster closer to the desired state.
98-
//
99-
// For more details, check Reconcile and its Result here:
100-
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
96+
// Reconcile advances a PVC's population and restore lifecycle.
10197
func (r *VolumePopulatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
10298
reqCtx := intctrlutil.RequestCtx{
10399
Ctx: ctx,
@@ -185,9 +181,9 @@ func (r *VolumePopulatorReconciler) mapRestoreToPVCs(ctx context.Context, obj cl
185181
}
186182
includeTerminal := !restore.DeletionTimestamp.IsZero() ||
187183
restore.Status.Phase == dpv1alpha1.RestorePhaseCompleted || restore.Status.Phase == dpv1alpha1.RestorePhaseFailed
188-
// A postReady Restore is owned by its target Component, while its labels
189-
// identify only the first source PVC that created it. Other Components in
190-
// the Cluster can wait on the same Restore through postReady redirection.
184+
// The component-name label identifies the first source PVC's Component,
185+
// while the owner reference identifies the target Component. Other
186+
// Components can wait on the same Restore through postReady redirection.
191187
return r.mapRestorePVCs(ctx, restore.Namespace, client.MatchingLabels{
192188
constant.AppInstanceLabelKey: clusterName,
193189
}, restore.Labels[dptypes.ClusterUIDLabelKey], includeTerminal)
@@ -203,10 +199,9 @@ func (r *VolumePopulatorReconciler) mapComponentToPVCs(ctx context.Context, obj
203199
if clusterName == "" || componentName == "" {
204200
return nil
205201
}
206-
// A PVC can depend on another Component through a redirected postReady
207-
// Restore. That relationship is derived from Backup status and is not
208-
// represented on the Component, so a Component event must fan out to all
209-
// active restore PVCs in its Cluster.
202+
// Normal Component updates can unblock redirected postReady restores in
203+
// other Components. Fan out to unfinished restore PVCs in the Cluster,
204+
// since those dependencies are not represented on the Component itself.
210205
labels := client.MatchingLabels{
211206
constant.AppInstanceLabelKey: clusterName,
212207
}
@@ -216,8 +211,8 @@ func (r *VolumePopulatorReconciler) mapComponentToPVCs(ctx context.Context, obj
216211
}
217212
includeTerminal := !comp.DeletionTimestamp.IsZero()
218213
if includeTerminal {
219-
// Component deletion is a termination signal only for restore PVCs
220-
// physically owned by that Component.
214+
// Deletion targets this Component's restore PVCs, including terminal
215+
// PVCs that may still have restore resources to clean up.
221216
labels[constant.KBAppComponentLabelKey] = componentName
222217
}
223218
return r.mapRestorePVCs(ctx, comp.Namespace, labels, string(clusterOwner.UID), includeTerminal)
@@ -423,9 +418,10 @@ func (r *VolumePopulatorReconciler) syncPVC(reqCtx intctrlutil.RequestCtx, pvc *
423418
return nil
424419
}
425420

426-
// handleRestoreParentLifecycle makes Cluster and Component dependencies part of
427-
// the PVC restore state machine. Parent deletion is the only cancellation
428-
// signal; the target PVC deletion timestamp is intentionally irrelevant.
421+
// handleRestoreParentLifecycle validates parent identity and protection, and
422+
// initiates restore termination when the Cluster or Component is deleting.
423+
// It returns true when normal restore processing should stop. Target PVC
424+
// deletion alone does not request restore termination.
429425
func (r *VolumePopulatorReconciler) handleRestoreParentLifecycle(reqCtx intctrlutil.RequestCtx,
430426
pvc *corev1.PersistentVolumeClaim) (bool, error) {
431427
clusterName := pvc.Labels[constant.AppInstanceLabelKey]
@@ -438,15 +434,13 @@ func (r *VolumePopulatorReconciler) handleRestoreParentLifecycle(reqCtx intctrlu
438434
cluster := &appsv1.Cluster{}
439435
clusterKey := types.NamespacedName{Namespace: pvc.Namespace, Name: clusterName}
440436
if err := r.Client.Get(reqCtx.Ctx, clusterKey, cluster); err != nil {
441-
// Unmarked PVCs may be standalone DP restores that happen to use app
442-
// labels. Marked PVCs are known Cluster restores and must retry.
437+
// App labels alone do not establish Cluster restore identity;
438+
// standalone DP restores may use the same labels.
443439
if apierrors.IsNotFound(err) && !hasClusterIdentity {
444440
return false, nil
445441
}
446-
// A retained target PVC can outlive its Cluster after its VP-owned
447-
// finalizer and side effects have been released. Do not restart or poll
448-
// the completed cancellation path merely because App-owned restore
449-
// identity remains on that PVC.
442+
// Do not restart a retained target after its Cluster is gone and VP
443+
// has released the target finalizer.
450444
if apierrors.IsNotFound(err) &&
451445
!controllerutil.ContainsFinalizer(pvc, dptypes.DataProtectionFinalizerName) {
452446
return true, nil

controllers/dataprotection/volumepopulator_controller_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3372,7 +3372,7 @@ func TestMapExecutionRestoreToTargetPVC(t *testing.T) {
33723372
require.Empty(t, reconciler.mapRestoreToPVCs(context.Background(), sourceRestore))
33733373
}
33743374

3375-
func TestMapPostReadyRestoreToNonTerminalComponentPVCs(t *testing.T) {
3375+
func TestMapPostReadyRestoreToNonTerminalClusterPVCs(t *testing.T) {
33763376
comp := &kbappsv1.Component{ObjectMeta: metav1.ObjectMeta{
33773377
Namespace: "default", Name: "cluster-mysql", UID: "component-uid",
33783378
Labels: map[string]string{
@@ -3610,7 +3610,7 @@ func TestParentDeletionTerminatesOnlyVolumePopulatorResourcesInOrder(t *testing.
36103610
require.True(t, apierrors.IsNotFound(cli.Get(context.Background(), client.ObjectKeyFromObject(helper), helper)))
36113611
require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(target), currentTarget))
36123612
require.Contains(t, currentTarget.Finalizers, dptypes.DataProtectionFinalizerName,
3613-
"target DP finalizer must remain until an API read confirms helper deletion")
3613+
"target DP finalizer must remain until helper deletion is observed")
36143614

36153615
terminated, err = reconciler.handleRestoreParentLifecycle(reqCtx, currentTarget)
36163616
require.True(t, terminated)
@@ -3620,7 +3620,7 @@ func TestParentDeletionTerminatesOnlyVolumePopulatorResourcesInOrder(t *testing.
36203620
require.Contains(t, currentTarget.Finalizers, "example.io/app-owner")
36213621
require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(cluster), cluster))
36223622
require.Contains(t, cluster.Finalizers, dptypes.RestoreProtectionFinalizerName,
3623-
"VolumePopulator must never remove the Cluster controller finalizer")
3623+
"VolumePopulator must not remove the Cluster restore-protection finalizer")
36243624
}
36253625

36263626
func TestClusterDeletionTerminatesPostReadyRestoreAfterComponentIsGone(t *testing.T) {
@@ -3714,7 +3714,7 @@ func TestComponentTerminationContinuesAfterRetainedPVCIsDetachedFromWorkload(t *
37143714
require.False(t, currentRestore.DeletionTimestamp.IsZero())
37153715
}
37163716

3717-
func TestITS2ScaleInRetainedPVCContinuesRestoreWithVerifiedIdentity(t *testing.T) {
3717+
func TestRetainedPVCContinuesRestoreWithVerifiedIdentity(t *testing.T) {
37183718
scheme, cluster, component, _, target := parentRestoreObjects(t)
37193719
target.OwnerReferences = nil
37203720
target.Finalizers = []string{dptypes.DataProtectionFinalizerName}
@@ -3739,7 +3739,7 @@ func TestITS2ScaleInRetainedPVCContinuesRestoreWithVerifiedIdentity(t *testing.T
37393739
}, helper), "verified retained target must continue the normal restore state machine")
37403740
}
37413741

3742-
func TestComponentReplacementFailsClosedBeforeRestoreProgression(t *testing.T) {
3742+
func TestVolumePopulatorRejectsMismatchedComponentUID(t *testing.T) {
37433743
scheme, cluster, component, _, target := parentRestoreObjects(t)
37443744
target.OwnerReferences = nil
37453745
target.Labels[dptypes.ComponentUIDLabelKey] = "previous-component-uid"
@@ -3823,7 +3823,7 @@ func TestComponentTerminationDeletesPostReadyRestoreByOwnerNotSourceLabel(t *tes
38233823
currentRestore := &dpv1alpha1.Restore{}
38243824
require.NoError(t, cli.Get(context.Background(), client.ObjectKeyFromObject(postReady), currentRestore))
38253825
require.False(t, currentRestore.DeletionTimestamp.IsZero(),
3826-
"the deleting owner Component must terminate postReady Restore regardless of source label")
3826+
"VolumePopulator must terminate postReady Restore when its owner Component is deleting, regardless of source label")
38273827
}
38283828

38293829
func TestTargetPVCDeletionIsNotParentTermination(t *testing.T) {

pkg/dataprotection/types/constant.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ const (
4646
const (
4747
// DataProtectionFinalizerName is the name of our custom finalizer
4848
DataProtectionFinalizerName = "dataprotection.kubeblocks.io/finalizer"
49-
// RestoreProtectionFinalizerName is owned independently by the Cluster
50-
// restore lifecycle controller.
51-
RestoreProtectionFinalizerName = "dataprotection.kubeblocks.io/restore-protection"
49+
// RestoreProtectionFinalizerName prevents Cluster deletion from completing
50+
// before its restore resources have been cleaned up.
51+
RestoreProtectionFinalizerName = "dataprotection.kubeblocks.io/restore-protection-finalizer"
5252
)
5353

5454
// annotation keys

0 commit comments

Comments
 (0)