From 67e30af2a200ce615aaff82c46065c52ca3a33a8 Mon Sep 17 00:00:00 2001 From: Gina Date: Tue, 23 Jun 2026 22:28:41 +0800 Subject: [PATCH 1/3] fix(pvc): preserve dataprotection restore volume bindings --- .../persistentvolumeclaims/syncer.go | 575 ++++++++- .../persistentvolumeclaims/syncer_test.go | 1118 ++++++++++++++++- .../persistentvolumeclaims/translate.go | 6 + .../persistentvolumes/syncer_test.go | 58 + .../resources/persistentvolumes/translate.go | 6 +- 5 files changed, 1717 insertions(+), 46 deletions(-) diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer.go b/pkg/controllers/resources/persistentvolumeclaims/syncer.go index f7f310e46b..5e11784b00 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer.go @@ -1,12 +1,15 @@ package persistentvolumeclaims import ( + "crypto/sha256" + "encoding/hex" "fmt" "time" storagev1 "k8s.io/api/storage/v1" "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/equality" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/klog/v2" @@ -42,6 +45,17 @@ const ( bindCompletedAnnotation = "pv.kubernetes.io/bind-completed" boundByControllerAnnotation = "pv.kubernetes.io/bound-by-controller" storageProvisionerAnnotation = "volume.beta.kubernetes.io/storage-provisioner" + selectedNodeAnnotation = "volume.kubernetes.io/selected-node" + + dataProtectionAPIGroup = "dataprotection.kubeblocks.io" + dataProtectionBackupKind = "Backup" + dataProtectionPopulateFromAnnotation = "dataprotection.kubeblocks.io/populate-from" + + dataProtectionMaterializationRequestLabel = "vcluster.loft.sh/dataprotection-materialization-request" + dataProtectionMaterializationRequestPrefix = "dp-host-materialization-" + dataProtectionMaterializationStatePending = "pending" + + dataProtectionRestoreConditionReasonProvisioned = "Provisioned" ) func New(ctx *synccontext.RegisterContext) (syncertypes.Object, error) { @@ -105,13 +119,42 @@ func (s *persistentVolumeClaimSyncer) SyncToHost(ctx *synccontext.SyncContext, e return ctrl.Result{}, nil } - if event.HostOld != nil || event.Virtual.DeletionTimestamp != nil { + preserveDeletingHostPVC, err := s.shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if event.HostOld != nil && preserveDeletingHostPVC && event.Virtual.DeletionTimestamp == nil { + // The host PVC was intentionally deleted so it can be recreated without + // the Backup dataSource. Keep the virtual restore PVC and continue into + // the create path below. + } else if event.HostOld != nil || event.Virtual.DeletionTimestamp != nil { return patcher.DeleteVirtualObjectWithOptions(ctx, event.Virtual, event.HostOld, "host object was deleted", &client.DeleteOptions{ GracePeriodSeconds: &zero, }) } - pObj, err := s.translate(ctx, event.Virtual) + pObj, handled, err := s.translateDataProtectionBackupToHost(ctx, event.Virtual) + if err != nil { + s.EventRecorder().Eventf( + event.Virtual, + nil, + "Warning", + "SyncError", + fmt.Sprintf("Sync%s", event.Virtual.GetObjectKind().GroupVersionKind().Kind), + err.Error(), + ) + return ctrl.Result{}, err + } + if handled { + err = pro.ApplyPatchesHostObject(ctx, nil, pObj, event.Virtual, ctx.Config.Sync.ToHost.PersistentVolumeClaims.Patches, false) + if err != nil { + return ctrl.Result{}, err + } + + return patcher.CreateHostObject(ctx, event.Virtual, pObj, s.EventRecorder(), true) + } + + pObj, err = s.translate(ctx, event.Virtual) if err != nil { s.EventRecorder().Eventf( event.Virtual, @@ -132,7 +175,7 @@ func (s *persistentVolumeClaimSyncer) SyncToHost(ctx *synccontext.SyncContext, e return patcher.CreateHostObject(ctx, event.Virtual, pObj, s.EventRecorder(), true) } -func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*corev1.PersistentVolumeClaim]) (_ ctrl.Result, retErr error) { +func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event *synccontext.SyncEvent[*corev1.PersistentVolumeClaim]) (result ctrl.Result, retErr error) { if s.applyLimitByClass(ctx, event.Virtual) { return ctrl.Result{}, nil } @@ -154,6 +197,13 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * // if pvs are deleted check the corresponding pvc is deleted as well if event.Host.DeletionTimestamp != nil { + preserveDeletingHostPVC, err := s.shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if preserveDeletingHostPVC { + return ctrl.Result{RequeueAfter: 2 * time.Second}, nil + } if event.Virtual.DeletionTimestamp == nil { return patcher.DeleteVirtualObjectWithOptions(ctx, event.Virtual, event.Host, "host persistent volume claim is being deleted", &client.DeleteOptions{GracePeriodSeconds: &minimumGracePeriodInSeconds}) } else if *event.Virtual.DeletionGracePeriodSeconds != *event.Host.DeletionGracePeriodSeconds { @@ -167,6 +217,13 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * Preconditions: metav1.NewUIDPreconditions(string(event.Host.UID)), }) } + recreateHostPVC, err := s.shouldRecreateDataProtectionHostNoDataRestorePVC(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if recreateHostPVC { + return deleteDataProtectionNoDataRestoreHostPVC(ctx, event.Host, event.Virtual) + } // make sure the persistent volume is synced / faked if event.Host.Spec.VolumeName != "" { @@ -176,6 +233,13 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * } else if requeue { return ctrl.Result{Requeue: true}, nil } + } else { + requeue, err := s.ensureDataProtectionPopulatedPersistentVolumeName(ctx, event.Host, event.Virtual, ctx.Log) + if err != nil { + return ctrl.Result{}, err + } else if requeue { + return ctrl.Result{Requeue: true}, nil + } } // patch objects @@ -188,6 +252,12 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * retErr = utilerrors.NewAggregate([]error{retErr, err}) } + if kerrors.IsConflict(retErr) { + result = ctrl.Result{RequeueAfter: time.Second} + retErr = nil + return + } + if retErr != nil { s.EventRecorder().Eventf( event.Virtual, @@ -205,7 +275,25 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * s.translateUpdateBackwards(event.Host, event.Virtual) // copy host status - event.Virtual.Status = *event.Host.Status.DeepCopy() + vPV, preserveVirtualStatus, err := s.dataProtectionPopulatedPersistentVolume(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if preserveVirtualStatus { + err = s.ensureDataProtectionHostMaterialization(ctx, event.Host, event.Virtual, vPV) + if err != nil { + return ctrl.Result{}, err + } + ensureDataProtectionVirtualPopulateStatus(event.Virtual, vPV) + } else { + preserveExternalPopulatorStatus, err := s.shouldPreserveExternalPopulatorVirtualStatus(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if !preserveExternalPopulatorStatus { + event.Virtual.Status = *event.Host.Status.DeepCopy() + } + } // allow storage size to be increased event.Host.Spec.Resources.Requests = event.Virtual.Spec.Resources.Requests @@ -232,6 +320,25 @@ func (s *persistentVolumeClaimSyncer) SyncToVirtual(ctx *synccontext.SyncContext return patcher.CreateVirtualObject(ctx, event.Host, vPvc, s.EventRecorder(), true) } +func (s *persistentVolumeClaimSyncer) translateDataProtectionBackupToHost(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, bool, error) { + noDataRestore, err := s.isDataProtectionNoDataRestorePVC(ctx, vObj) + if err != nil { + return nil, true, err + } + if !noDataRestore { + return nil, false, nil + } + + pObj, err := s.translate(ctx, vObj) + if err != nil { + return nil, true, err + } + + clearDataProtectionHostDataSource(pObj) + pObj.Spec.VolumeName = "" + return pObj, true, nil +} + func (s *persistentVolumeClaimSyncer) ensurePersistentVolume(ctx *synccontext.SyncContext, pObj *corev1.PersistentVolumeClaim, vObj *corev1.PersistentVolumeClaim, log loghelper.Logger) (bool, error) { // ensure the persistent volume is available in the virtual cluster vPV := &corev1.PersistentVolume{} @@ -283,12 +390,472 @@ func (s *persistentVolumeClaimSyncer) ensurePersistentVolume(ctx *synccontext.Sy if err != nil { return false, err } + + // The direct update changes the virtual PVC resourceVersion. Stop this + // reconcile here so the following status patch uses a fresh object. + return true, nil } } return false, nil } +func (s *persistentVolumeClaimSyncer) ensureDataProtectionPopulatedPersistentVolumeName(ctx *synccontext.SyncContext, pObj *corev1.PersistentVolumeClaim, vObj *corev1.PersistentVolumeClaim, log loghelper.Logger) (bool, error) { + if vObj.Spec.VolumeName != "" || !isDataProtectionBackupPVC(vObj) || !isHostPVCWaitingForVolume(pObj) { + return false, nil + } + + vPV, ok, err := s.findDataProtectionPopulatedPersistentVolumeByClaimRef(ctx, vObj) + if err != nil || !ok { + return false, err + } + + log.Infof("update virtual data protection pvc %s/%s volume name to populated pv %s", vObj.Namespace, vObj.Name, vPV.Name) + vObj.Spec.VolumeName = vPV.Name + err = ctx.VirtualClient.Update(ctx, vObj) + if err != nil { + return false, err + } + + // The direct update changes the virtual PVC resourceVersion. Stop this + // reconcile here so the following status patch uses a fresh object. + return true, nil +} + +func (s *persistentVolumeClaimSyncer) findDataProtectionPopulatedPersistentVolumeByClaimRef(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { + vPVs := &corev1.PersistentVolumeList{} + err := ctx.VirtualClient.List(ctx.Context, vPVs) + if err != nil { + return nil, false, err + } + + var match *corev1.PersistentVolume + for i := range vPVs.Items { + vPV := &vPVs.Items[i] + if !isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, true) { + continue + } + if match != nil && match.Name != vPV.Name { + return nil, false, fmt.Errorf("multiple data protection populated persistent volumes match pvc %s/%s", vObj.Namespace, vObj.Name) + } + match = vPV.DeepCopy() + } + + if match == nil { + return nil, false, nil + } + + return match, true, nil +} + +func (s *persistentVolumeClaimSyncer) ensureDataProtectionHostMaterialization(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { + hostPVName := s.dataProtectionHostPersistentVolumeName(ctx, vPV) + hostPV := &corev1.PersistentVolume{} + err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{Name: hostPVName}, hostPV) + if err != nil { + if kerrors.IsNotFound(err) { + return s.upsertDataProtectionMaterializationRequest(ctx, pObj, vObj, vPV) + } + return err + } + + helperPVC, helperFound, err := s.findDataProtectionPopulateHelperPVC(ctx, vObj, vPV) + if err != nil { + return err + } + + pObj.Spec.VolumeName = hostPVName + return s.ensureDataProtectionHostPVClaimRef(ctx, hostPVName, pObj, helperPVC, helperFound) +} + +func (s *persistentVolumeClaimSyncer) upsertDataProtectionMaterializationRequest(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { + desired := dataProtectionMaterializationRequest(ctx.Config.HostNamespace, pObj, vObj, vPV) + existing := &corev1.ConfigMap{} + err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{ + Namespace: desired.Namespace, + Name: desired.Name, + }, existing) + if err != nil { + if kerrors.IsNotFound(err) { + return ctx.HostClient.Create(ctx.Context, desired) + } + return err + } + + updated := existing.DeepCopy() + updated.Labels = desired.Labels + updated.Data = desired.Data + if equality.Semantic.DeepEqual(existing.Labels, updated.Labels) && + equality.Semantic.DeepEqual(existing.Data, updated.Data) { + return nil + } + + return ctx.HostClient.Patch(ctx.Context, updated, client.MergeFrom(existing)) +} + +func (s *persistentVolumeClaimSyncer) dataProtectionHostPersistentVolumeName(ctx *synccontext.SyncContext, vPV *corev1.PersistentVolume) string { + if s.useFakePersistentVolumes { + return vPV.Name + } + + return mappings.VirtualToHostName(ctx, vPV.Name, "", mappings.PersistentVolumes()) +} + +func (s *persistentVolumeClaimSyncer) findDataProtectionPopulateHelperPVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) (*corev1.PersistentVolumeClaim, bool, error) { + pvcList := &corev1.PersistentVolumeClaimList{} + err := ctx.VirtualClient.List(ctx.Context, pvcList, client.InNamespace(vObj.Namespace)) + if err != nil { + return nil, false, err + } + + var match *corev1.PersistentVolumeClaim + for i := range pvcList.Items { + pvc := &pvcList.Items[i] + if pvc.Name == vObj.Name || pvc.Spec.VolumeName != vPV.Name { + continue + } + if match != nil && match.Name != pvc.Name { + return nil, false, fmt.Errorf("multiple data protection helper persistent volume claims match pv %s for pvc %s/%s", vPV.Name, vObj.Namespace, vObj.Name) + } + match = pvc.DeepCopy() + } + if match == nil { + return nil, false, nil + } + + return match, true, nil +} + +func (s *persistentVolumeClaimSyncer) ensureDataProtectionHostPVClaimRef(ctx *synccontext.SyncContext, hostPVName string, pObj *corev1.PersistentVolumeClaim, helperPVC *corev1.PersistentVolumeClaim, helperFound bool) error { + hostPV := &corev1.PersistentVolume{} + err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{Name: hostPVName}, hostPV) + if err != nil { + return err + } + + targetRef := &corev1.ObjectReference{ + APIVersion: corev1.SchemeGroupVersion.Version, + Kind: "PersistentVolumeClaim", + Namespace: pObj.Namespace, + Name: pObj.Name, + UID: pObj.UID, + } + if claimRefReferencesPersistentVolumeClaim(hostPV.Spec.ClaimRef, pObj) { + if hostPV.Spec.ClaimRef.UID == pObj.UID { + return nil + } + } else if hostPV.Spec.ClaimRef != nil { + if !helperFound { + return fmt.Errorf("host pv %s is bound to %s/%s, but no virtual populate helper pvc was found for target pvc %s/%s", hostPVName, hostPV.Spec.ClaimRef.Namespace, hostPV.Spec.ClaimRef.Name, pObj.Namespace, pObj.Name) + } + ok, err := s.hostPVClaimRefMatchesVirtualPVC(ctx, hostPV.Spec.ClaimRef, helperPVC) + if err != nil { + return err + } else if !ok { + return fmt.Errorf("host pv %s claimRef %s/%s does not match target pvc %s/%s or expected populate helper pvc %s/%s", hostPVName, hostPV.Spec.ClaimRef.Namespace, hostPV.Spec.ClaimRef.Name, pObj.Namespace, pObj.Name, helperPVC.Namespace, helperPVC.Name) + } + } + + updated := hostPV.DeepCopy() + updated.Spec.ClaimRef = targetRef + return ctx.HostClient.Patch(ctx.Context, updated, client.MergeFrom(hostPV)) +} + +func (s *persistentVolumeClaimSyncer) hostPVClaimRefMatchesVirtualPVC(ctx *synccontext.SyncContext, ref *corev1.ObjectReference, vPVC *corev1.PersistentVolumeClaim) (bool, error) { + if ref == nil || vPVC == nil { + return false, nil + } + + hostPVCName := s.VirtualToHost(ctx, types.NamespacedName{Name: vPVC.Name, Namespace: vPVC.Namespace}, vPVC) + if ref.Namespace != hostPVCName.Namespace || ref.Name != hostPVCName.Name { + return false, nil + } + + hostPVC := &corev1.PersistentVolumeClaim{} + err := ctx.HostClient.Get(ctx.Context, hostPVCName, hostPVC) + if err != nil { + if kerrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + return ref.UID == "" || ref.UID == hostPVC.UID, nil +} + +func claimRefMatchesPersistentVolumeClaim(ref *corev1.ObjectReference, pvc *corev1.PersistentVolumeClaim) bool { + if ref == nil || pvc == nil { + return false + } + + return claimRefReferencesPersistentVolumeClaim(ref, pvc) && + (ref.UID == "" || ref.UID == pvc.UID) +} + +func claimRefReferencesPersistentVolumeClaim(ref *corev1.ObjectReference, pvc *corev1.PersistentVolumeClaim) bool { + if ref == nil || pvc == nil { + return false + } + + return ref.Namespace == pvc.Namespace && ref.Name == pvc.Name +} + +func (s *persistentVolumeClaimSyncer) dataProtectionPopulatedPersistentVolume(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { + if !isDataProtectionBackupPVC(vObj) || vObj.Spec.VolumeName == "" || !isHostPVCWaitingForVolume(pObj) { + return nil, false, nil + } + + vPV := &corev1.PersistentVolume{} + err := ctx.VirtualClient.Get(ctx, types.NamespacedName{Name: vObj.Spec.VolumeName}, vPV) + if err != nil { + if kerrors.IsNotFound(err) { + return nil, false, nil + } + return nil, false, err + } + + if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { + return nil, false, nil + } + if !isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, false) { + return nil, false, nil + } + if !isVirtualPVCBound(vObj) && vPV.Status.Phase != corev1.VolumeBound { + return nil, false, nil + } + + return vPV, true, nil +} + +func (s *persistentVolumeClaimSyncer) shouldPreserveExternalPopulatorVirtualStatus(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !hasExternalPopulatorDataSource(vObj) { + return false, nil + } + if !isDataProtectionBackupPVC(vObj) { + return true, nil + } + if !isHostPVCWaitingForVolume(pObj) { + return false, nil + } + if vObj.Spec.VolumeName == "" { + return true, nil + } + + vPV := &corev1.PersistentVolume{} + err := ctx.VirtualClient.Get(ctx, types.NamespacedName{Name: vObj.Spec.VolumeName}, vPV) + if err != nil { + if kerrors.IsNotFound(err) { + return true, nil + } + return false, err + } + if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { + return true, nil + } + + return isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, false), nil +} + +func ensureDataProtectionVirtualPopulateStatus(vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) { + if isVirtualPVCBound(vObj) { + return + } + + if vPV.Status.Phase != corev1.VolumeBound { + return + } + + vObj.Status.Phase = corev1.ClaimBound + if len(vObj.Status.AccessModes) == 0 { + vObj.Status.AccessModes = append([]corev1.PersistentVolumeAccessMode(nil), vPV.Spec.AccessModes...) + } + storage, ok := vObj.Status.Capacity[corev1.ResourceStorage] + if vObj.Status.Capacity == nil || !ok || storage.IsZero() { + vObj.Status.Capacity = vPV.Spec.Capacity.DeepCopy() + } +} + +func isDataProtectionPopulatedPersistentVolumeForPVC(vPV *corev1.PersistentVolume, vObj *corev1.PersistentVolumeClaim, requireBoundPV bool) bool { + if requireBoundPV && vPV.Status.Phase != corev1.VolumeBound { + return false + } + if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { + return false + } + if vPV.Spec.ClaimRef == nil || + vPV.Spec.ClaimRef.Namespace != vObj.Namespace || + vPV.Spec.ClaimRef.Name != vObj.Name { + return false + } + if vPV.Spec.ClaimRef.UID != "" && vPV.Spec.ClaimRef.UID != vObj.UID { + return false + } + + return true +} + +func dataProtectionMaterializationRequest(hostNamespace string, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: hostNamespace, + Name: dataProtectionMaterializationRequestName(pObj), + Labels: map[string]string{ + dataProtectionMaterializationRequestLabel: "true", + }, + }, + Data: map[string]string{ + "state": dataProtectionMaterializationStatePending, + "hostPVCNamespace": pObj.Namespace, + "hostPVCName": pObj.Name, + "virtualPVCNamespace": vObj.Namespace, + "virtualPVCName": vObj.Name, + "virtualPVCUID": string(vObj.UID), + "virtualPVName": vPV.Name, + "virtualPVUID": string(vPV.UID), + "backupName": vObj.Spec.DataSourceRef.Name, + "populateFrom": vPV.Annotations[dataProtectionPopulateFromAnnotation], + }, + } +} + +func dataProtectionMaterializationRequestName(pObj *corev1.PersistentVolumeClaim) string { + sum := sha256.Sum256([]byte(pObj.Namespace + "/" + pObj.Name)) + return dataProtectionMaterializationRequestPrefix + hex.EncodeToString(sum[:])[:16] +} + +func isDataProtectionBackupPVC(pvc *corev1.PersistentVolumeClaim) bool { + if pvc.Spec.DataSourceRef == nil || pvc.Spec.DataSourceRef.APIGroup == nil { + return false + } + + return *pvc.Spec.DataSourceRef.APIGroup == dataProtectionAPIGroup && + pvc.Spec.DataSourceRef.Kind == dataProtectionBackupKind && + pvc.Spec.DataSourceRef.Name != "" +} + +func hasExternalPopulatorDataSource(pvc *corev1.PersistentVolumeClaim) bool { + if pvc.Spec.DataSourceRef == nil { + return false + } + + switch pvc.Spec.DataSourceRef.Kind { + case "VolumeSnapshot", "PersistentVolumeClaim": + return false + default: + return true + } +} + +func isVirtualPVCBound(pvc *corev1.PersistentVolumeClaim) bool { + if pvc.Spec.VolumeName == "" || pvc.Status.Phase != corev1.ClaimBound { + return false + } + + storage, ok := pvc.Status.Capacity[corev1.ResourceStorage] + return ok && !storage.IsZero() +} + +func isHostPVCWaitingForVolume(pvc *corev1.PersistentVolumeClaim) bool { + if pvc.Status.Phase == corev1.ClaimBound { + return false + } + + storage, ok := pvc.Status.Capacity[corev1.ResourceStorage] + return !ok || storage.IsZero() +} + +func isDataProtectionRestoreProvisionedWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { + for _, condition := range pvc.Status.Conditions { + if condition.Type == corev1.PersistentVolumeClaimConditionType("Restore") && + condition.Status == corev1.ConditionTrue && + condition.Reason == dataProtectionRestoreConditionReasonProvisioned { + return true + } + } + + return false +} + +func clearDataProtectionHostDataSource(pvc *corev1.PersistentVolumeClaim) { + pvc.Spec.DataSource = nil + pvc.Spec.DataSourceRef = nil +} + +func (s *persistentVolumeClaimSyncer) shouldRecreateDataProtectionHostNoDataRestorePVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !isHostPVCWaitingForVolume(pObj) || + (!isDataProtectionBackupDataSource(pObj.Spec.DataSource) && !isDataProtectionBackupDataSourceRef(pObj.Spec.DataSourceRef)) { + return false, nil + } + + noDataRestore, err := s.isDataProtectionNoDataRestorePVC(ctx, vObj) + if err != nil || !noDataRestore { + return false, err + } + + return true, nil +} + +func (s *persistentVolumeClaimSyncer) shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + return s.isDataProtectionNoDataRestorePVC(ctx, vObj) +} + +func (s *persistentVolumeClaimSyncer) isDataProtectionNoDataRestorePVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !isDataProtectionBackupPVC(vObj) { + return false, nil + } + + return isDataProtectionRestoreProvisionedWithoutDataRestore(vObj), nil +} + +func deleteDataProtectionNoDataRestoreHostPVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (ctrl.Result, error) { + result, err := patcher.DeleteHostObjectWithOptions(ctx, pObj, vObj, "data protection restore pvc was provisioned without data restore", &client.DeleteOptions{ + GracePeriodSeconds: &zero, + Preconditions: hostDeletePreconditions(pObj), + }) + if kerrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + + return result, err +} + +func hostDeletePreconditions(obj client.Object) *metav1.Preconditions { + preconditions := &metav1.Preconditions{} + if obj.GetUID() != "" { + uid := obj.GetUID() + preconditions.UID = &uid + } + if obj.GetResourceVersion() != "" { + resourceVersion := obj.GetResourceVersion() + preconditions.ResourceVersion = &resourceVersion + } + if preconditions.UID == nil && preconditions.ResourceVersion == nil { + return nil + } + + return preconditions +} + +func isDataProtectionBackupDataSource(ref *corev1.TypedLocalObjectReference) bool { + if ref == nil || ref.APIGroup == nil { + return false + } + + return *ref.APIGroup == dataProtectionAPIGroup && + ref.Kind == dataProtectionBackupKind && + ref.Name != "" +} + +func isDataProtectionBackupDataSourceRef(ref *corev1.TypedObjectReference) bool { + if ref == nil || ref.APIGroup == nil { + return false + } + + return *ref.APIGroup == dataProtectionAPIGroup && + ref.Kind == dataProtectionBackupKind && + ref.Name != "" +} + func (s *persistentVolumeClaimSyncer) isHostVolumeRestoreInProgress(ctx *synccontext.SyncContext, pObj types.NamespacedName) (bool, error) { configMaps := &corev1.ConfigMapList{} err := ctx.HostClient.List(ctx.Context, configMaps, client.InNamespace(ctx.Config.HostNamespace), client.MatchingLabels{ diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go index bfcbbd7cb3..434bfae668 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go @@ -107,6 +107,7 @@ func TestSync(t *testing.T) { bindCompletedAnnotation: "testannotation", boundByControllerAnnotation: "testannotation2", storageProvisionerAnnotation: "testannotation3", + selectedNodeAnnotation: "node1", }, Labels: pObjectMeta.Labels, }, @@ -119,6 +120,7 @@ func TestSync(t *testing.T) { bindCompletedAnnotation: "testannotation", boundByControllerAnnotation: "testannotation2", storageProvisionerAnnotation: "testannotation3", + selectedNodeAnnotation: "node1", }, }, } @@ -136,6 +138,217 @@ func TestSync(t *testing.T) { Spec: backwardUpdateStatusPvc.Spec, Status: backwardUpdateStatusPvc.Status, } + backwardUpdateVolumeNameOnlyPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: vObjectMeta, + Spec: backwardUpdateStatusPvc.Spec, + } + dataProtectionGroup := dataProtectionAPIGroup + dataProtectionBackupPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: vObjectMeta.Name, + Namespace: vObjectMeta.Namespace, + UID: types.UID("target-pvc-uid"), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "restore-populated-pv", + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: dataProtectionBackupKind, + Name: "backup-1", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + } + dataProtectionBackupPendingPvc := dataProtectionBackupPvc.DeepCopy() + dataProtectionBackupPendingPvc.Spec.VolumeName = "" + dataProtectionBackupPendingPvc.Status = corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + } + dataProtectionBackupPendingPvcWithVolumeName := dataProtectionBackupPendingPvc.DeepCopy() + dataProtectionBackupPendingPvcWithVolumeName.Spec.VolumeName = "restore-populated-pv" + dataProtectionBackupPendingPvcWithVolumeNameBoundStatus := dataProtectionBackupPendingPvcWithVolumeName.DeepCopy() + dataProtectionBackupPendingPvcWithVolumeNameBoundStatus.Status = corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + } + dataProtectionHostPendingPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: pObjectMeta, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + Capacity: corev1.ResourceList{}, + }, + } + dataProtectionHostPendingPvcWithFakeVolumeName := dataProtectionHostPendingPvc.DeepCopy() + dataProtectionHostPendingPvcWithFakeVolumeName.Spec.VolumeName = "restore-populated-pv" + dataProtectionHostPendingPvcWithUID := dataProtectionHostPendingPvc.DeepCopy() + dataProtectionHostPendingPvcWithUID.Annotations[translate.UIDAnnotation] = string(dataProtectionBackupPvc.UID) + dataProtectionHostPendingPvcWithFakeVolumeNameAndUID := dataProtectionHostPendingPvcWithFakeVolumeName.DeepCopy() + dataProtectionHostPendingPvcWithFakeVolumeNameAndUID.Annotations[translate.UIDAnnotation] = string(dataProtectionBackupPvc.UID) + dataProtectionPopulatedPV := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "restore-populated-pv", + Annotations: map[string]string{ + dataProtectionPopulateFromAnnotation: "backup-1", + }, + }, + Spec: corev1.PersistentVolumeSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + ClaimRef: &corev1.ObjectReference{ + Namespace: dataProtectionBackupPvc.Namespace, + Name: dataProtectionBackupPvc.Name, + UID: dataProtectionBackupPvc.UID, + }, + }, + Status: corev1.PersistentVolumeStatus{ + Phase: corev1.VolumeBound, + }, + } + dataProtectionStaleUIDPvc := dataProtectionBackupPvc.DeepCopy() + dataProtectionStaleUIDPvc.Status = *dataProtectionHostPendingPvc.Status.DeepCopy() + dataProtectionStaleUIDPV := dataProtectionPopulatedPV.DeepCopy() + dataProtectionStaleUIDPV.Spec.ClaimRef.UID = types.UID("stale-pvc-uid") + dataProtectionStaleUIDPendingPvc := dataProtectionBackupPendingPvc.DeepCopy() + dataProtectionMaterializationRequestCM := dataProtectionMaterializationRequest("test", dataProtectionHostPendingPvc, dataProtectionBackupPvc, dataProtectionPopulatedPV) + dataProtectionNoDataRestorePvc := dataProtectionBackupPvc.DeepCopy() + dataProtectionNoDataRestorePvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ + { + Type: corev1.PersistentVolumeClaimConditionType("Restore"), + Status: corev1.ConditionTrue, + Reason: dataProtectionRestoreConditionReasonProvisioned, + }, + } + dataProtectionNoDataHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionNoDataHostPvc.Spec = corev1.PersistentVolumeClaimSpec{} + dataProtectionNoDataHostPvc.Status = dataProtectionNoDataRestorePvc.Status + dataProtectionNoDataHostPendingWithBackupSource := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionNoDataHostPendingWithBackupSource.Spec = corev1.PersistentVolumeClaimSpec{ + DataSource: &corev1.TypedLocalObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: dataProtectionBackupKind, + Name: "backup-1", + }, + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: dataProtectionBackupKind, + Name: "backup-1", + }, + } + dataProtectionNoDataHostPendingWithBackupSource.ResourceVersion = "1" + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.Spec.VolumeName = dataProtectionPopulatedPV.Name + dataProtectionDataRestoreHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionDataRestoreHostPvc.Spec = corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: dataProtectionBackupKind, + Name: "backup-1", + }, + } + dataProtectionNoDataHostDeletingWithBackupSource := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostDeletingWithBackupSource.Finalizers = []string{"kubernetes.io/pvc-protection"} + dataProtectionNoDataHostDeletingWithBackupSource.DeletionTimestamp = &metav1.Time{Time: time.Now()} + dataProtectionNoDataHostBoundWithBackupSource := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostBoundWithBackupSource.ResourceVersion = "2" + dataProtectionNoDataHostBoundWithBackupSource.Spec.VolumeName = "restore-populated-pv" + dataProtectionNoDataHostBoundWithBackupSource.Status = corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + } + dataProtectionNoDataRestorePvcWithHostBoundStatus := dataProtectionNoDataRestorePvc.DeepCopy() + dataProtectionNoDataRestorePvcWithHostBoundStatus.Status = *dataProtectionNoDataHostBoundWithBackupSource.Status.DeepCopy() + dataProtectionNoDataHostPendingWithoutBackupSource := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionNoDataHostPendingWithoutBackupSource.Spec = corev1.PersistentVolumeClaimSpec{} + dataProtectionNoDataHostDeletingWithoutBackupSource := dataProtectionNoDataHostPendingWithoutBackupSource.DeepCopy() + dataProtectionNoDataHostDeletingWithoutBackupSource.Finalizers = []string{"kubernetes.io/pvc-protection"} + dataProtectionNoDataHostDeletingWithoutBackupSource.DeletionTimestamp = &metav1.Time{Time: time.Now()} + + dataProtectionPopulateHelperPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kb-populate-target-pvc-uid", + Namespace: dataProtectionBackupPvc.Namespace, + UID: types.UID("populate-helper-pvc-uid"), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: dataProtectionPopulatedPV.Name, + }, + } + dataProtectionHostPopulateHelperPvcName := translate.Default.HostName(nil, dataProtectionPopulateHelperPvc.Name, dataProtectionPopulateHelperPvc.Namespace) + dataProtectionHostPopulateHelperPvcName.Namespace = pObjectMeta.Namespace + dataProtectionHostPopulateHelperPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: dataProtectionHostPopulateHelperPvcName.Name, + Namespace: dataProtectionHostPopulateHelperPvcName.Namespace, + UID: types.UID("host-populate-helper-pvc-uid"), + Annotations: map[string]string{ + translate.NameAnnotation: dataProtectionPopulateHelperPvc.Name, + translate.NamespaceAnnotation: dataProtectionPopulateHelperPvc.Namespace, + translate.UIDAnnotation: string(dataProtectionPopulateHelperPvc.UID), + translate.KindAnnotation: corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim").String(), + translate.HostNamespaceAnnotation: dataProtectionHostPopulateHelperPvcName.Namespace, + translate.HostNameAnnotation: dataProtectionHostPopulateHelperPvcName.Name, + }, + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + translate.NamespaceLabel: dataProtectionPopulateHelperPvc.Namespace, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: dataProtectionPopulatedPV.Name, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + } + dataProtectionHostPVBoundToHelper := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: dataProtectionPopulatedPV.Name, + }, + Spec: corev1.PersistentVolumeSpec{ + ClaimRef: &corev1.ObjectReference{ + APIVersion: corev1.SchemeGroupVersion.Version, + Kind: "PersistentVolumeClaim", + Namespace: dataProtectionHostPopulateHelperPvc.Namespace, + Name: dataProtectionHostPopulateHelperPvc.Name, + UID: dataProtectionHostPopulateHelperPvc.UID, + }, + }, + Status: corev1.PersistentVolumeStatus{ + Phase: corev1.VolumeBound, + }, + } + dataProtectionHostPVBoundToTarget := dataProtectionHostPVBoundToHelper.DeepCopy() + dataProtectionHostPVBoundToTarget.Spec.ClaimRef = &corev1.ObjectReference{ + APIVersion: corev1.SchemeGroupVersion.Version, + Kind: "PersistentVolumeClaim", + Namespace: dataProtectionHostPendingPvcWithUID.Namespace, + Name: dataProtectionHostPendingPvcWithUID.Name, + } + dataProtectionHostMaterializedTargetPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionHostMaterializedTargetPvc.Spec.VolumeName = dataProtectionPopulatedPV.Name + dataProtectionHostPendingPvcWithObjectUID := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionHostPendingPvcWithObjectUID.UID = types.UID("host-target-pvc-uid") + dataProtectionHostMaterializedTargetPvcWithObjectUID := dataProtectionHostMaterializedTargetPvc.DeepCopy() + dataProtectionHostMaterializedTargetPvcWithObjectUID.UID = dataProtectionHostPendingPvcWithObjectUID.UID + dataProtectionHostPVBoundToTargetStaleUID := dataProtectionHostPVBoundToTarget.DeepCopy() + dataProtectionHostPVBoundToTargetStaleUID.Spec.ClaimRef.UID = types.UID("stale-host-target-pvc-uid") + dataProtectionHostPVBoundToTargetFreshUID := dataProtectionHostPVBoundToTarget.DeepCopy() + dataProtectionHostPVBoundToTargetFreshUID.Spec.ClaimRef.UID = dataProtectionHostPendingPvcWithObjectUID.UID syncertesting.RunTestsWithContext(t, func(vConfig *config.VirtualClusterConfig, pClient *testingutil.FakeIndexClient, vClient *testingutil.FakeIndexClient) *synccontext.RegisterContext { ctx := syncertesting.NewFakeRegisterContext(vConfig, pClient, vClient) @@ -157,6 +370,80 @@ func TestSync(t *testing.T) { assert.NilError(t, err) }, }, + { + Name: "Create data protection data restore forward with host backup data source", + InitialVirtualState: []runtime.Object{dataProtectionBackupPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPendingPvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionDataRestoreHostPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).SyncToHost(syncCtx, synccontext.NewSyncToHostEvent(dataProtectionBackupPendingPvc.DeepCopy())) + assert.NilError(t, err) + }, + }, + { + Name: "Create data protection no-data restore forward without host backup data source", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).SyncToHost(syncCtx, synccontext.NewSyncToHostEvent(dataProtectionNoDataRestorePvc.DeepCopy())) + assert.NilError(t, err) + }, + }, + { + Name: "Recreate data protection no-data host pvc without deleting virtual after stale host pvc was deleted", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).SyncToHost(syncCtx, &synccontext.SyncToHostEvent[*corev1.PersistentVolumeClaim]{ + HostOld: dataProtectionNoDataHostDeletingWithBackupSource.DeepCopy(), + Virtual: dataProtectionNoDataRestorePvc.DeepCopy(), + }) + assert.NilError(t, err) + }, + }, + { + Name: "Recreate data protection no-data host pvc without deleting virtual after cleared host pvc was deleted", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).SyncToHost(syncCtx, &synccontext.SyncToHostEvent[*corev1.PersistentVolumeClaim]{ + HostOld: dataProtectionNoDataHostDeletingWithoutBackupSource.DeepCopy(), + Virtual: dataProtectionNoDataRestorePvc.DeepCopy(), + }) + assert.NilError(t, err) + }, + }, { Name: "Delete forward with create function", InitialVirtualState: []runtime.Object{basePvc}, @@ -239,6 +526,54 @@ func TestSync(t *testing.T) { assert.NilError(t, err) }, }, + { + Name: "Do not delete virtual data protection no-data pvc while stale host pvc is deleting", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostDeletingWithBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostDeletingWithBackupSource.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostDeletingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostDeletingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, result.RequeueAfter > 0) + }, + }, + { + Name: "Do not delete virtual data protection no-data pvc while cleared host pvc is deleting", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostDeletingWithoutBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostDeletingWithoutBackupSource.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostDeletingWithoutBackupSource.DeepCopy(), + dataProtectionNoDataHostDeletingWithoutBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, result.RequeueAfter > 0) + }, + }, { Name: "Update backwards new annotations", InitialVirtualState: []runtime.Object{basePvc}, @@ -267,9 +602,131 @@ func TestSync(t *testing.T) { }, }, { - Name: "Update backwards new status", + Name: "Delete existing host backup data source pvc after no-data restore is provisioned", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostPendingWithBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Preserve existing data restore host backup data source pvc with stale fake volume name after populated pv exists", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPendingPvcWithVolumeNameBoundStatus.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.DeepCopy(), + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Do not delete host backup data source pvc from stale pending snapshot after it is bound", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostBoundWithBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostBoundWithBackupSource.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, result.Requeue) + }, + }, + { + Name: "Do not delete current bound host backup data source pvc", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostBoundWithBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvcWithHostBoundStatus.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostBoundWithBackupSource.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostBoundWithBackupSource.DeepCopy(), + dataProtectionNoDataHostBoundWithBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, !result.Requeue) + }, + }, + { + Name: "Requeue after updating virtual pvc volume name from host", InitialVirtualState: []runtime.Object{basePvc.DeepCopy()}, InitialPhysicalState: []runtime.Object{backwardUpdateStatusPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {backwardUpdateVolumeNameOnlyPvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {backwardUpdateStatusPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + backwardUpdateStatusPvc.DeepCopy(), + backwardUpdateStatusPvc.DeepCopy(), + basePvc.DeepCopy(), + basePvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, result.Requeue) + }, + }, + { + Name: "Update backwards new status", + InitialVirtualState: []runtime.Object{backwardUpdateVolumeNameOnlyPvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{backwardUpdateStatusPvc.DeepCopy()}, ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {backwardUpdatedStatusPvc.DeepCopy()}, }, @@ -282,8 +739,8 @@ func TestSync(t *testing.T) { pObjOld := backwardUpdateStatusPvc.DeepCopy() pObj := backwardUpdateStatusPvc.DeepCopy() - vObjOld := basePvc.DeepCopy() - vObj := basePvc.DeepCopy() + vObjOld := backwardUpdateVolumeNameOnlyPvc.DeepCopy() + vObj := backwardUpdateVolumeNameOnlyPvc.DeepCopy() _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( pObjOld, @@ -295,55 +752,296 @@ func TestSync(t *testing.T) { }, }, { - Name: "Recreate pvc if volume name is different", + Name: "Preserve data protection populated virtual status while host pvc waits for volume", InitialVirtualState: []runtime.Object{ - &corev1.PersistentVolumeClaim{ - ObjectMeta: basePvc.ObjectMeta, - Spec: corev1.PersistentVolumeClaimSpec{ - VolumeName: "test", - }, - }, - }, - InitialPhysicalState: []runtime.Object{ - &corev1.PersistentVolumeClaim{ - ObjectMeta: pObjectMeta, - Spec: corev1.PersistentVolumeClaimSpec{ - VolumeName: "test2", - }, - }, + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ - corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { - &corev1.PersistentVolumeClaim{ - ObjectMeta: basePvc.ObjectMeta, - Spec: corev1.PersistentVolumeClaimSpec{ - VolumeName: "test2", - }, - }, - }, + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, }, ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ - corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { - &corev1.PersistentVolumeClaim{ - ObjectMeta: pObjectMeta, - Spec: corev1.PersistentVolumeClaimSpec{ - VolumeName: "test2", - }, - }, - }, + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvcWithUID.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("ConfigMap"): {dataProtectionMaterializationRequestCM.DeepCopy()}, }, Sync: func(ctx *synccontext.RegisterContext) { syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true - vPVC := &corev1.PersistentVolumeClaim{} - err := syncCtx.VirtualClient.Get(syncCtx, types.NamespacedName{ - Namespace: basePvc.Namespace, - Name: basePvc.Name, - }, vPVC) + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + )) assert.NilError(t, err) - - pPVC := &corev1.PersistentVolumeClaim{} + }, + }, + { + Name: "Bridge data protection populated host pv from helper pvc to target pvc", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + dataProtectionPopulateHelperPvc.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{ + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPopulateHelperPvc.DeepCopy(), + dataProtectionHostPVBoundToHelper.DeepCopy(), + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulateHelperPvc.DeepCopy(), + }, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + dataProtectionHostMaterializedTargetPvc.DeepCopy(), + dataProtectionHostPopulateHelperPvc.DeepCopy(), + }, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionHostPVBoundToTarget.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Refresh stale data protection host pv target claim ref uid", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + dataProtectionPopulateHelperPvc.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{ + dataProtectionHostPendingPvcWithObjectUID.DeepCopy(), + dataProtectionHostPVBoundToTargetStaleUID.DeepCopy(), + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulateHelperPvc.DeepCopy(), + }, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostMaterializedTargetPvcWithObjectUID.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionHostPVBoundToTargetFreshUID.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvcWithObjectUID.DeepCopy(), + dataProtectionHostPendingPvcWithObjectUID.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Requeue after deriving data protection pvc volume name from populated pv claim ref", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPendingPvc.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPendingPvcWithVolumeName.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPendingPvc.DeepCopy(), + dataProtectionBackupPendingPvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, result.Requeue) + }, + }, + { + Name: "Derive data protection populated virtual status from bound populated pv while host pvc waits", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPendingPvcWithVolumeNameBoundStatus.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvcWithUID.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("ConfigMap"): {dataProtectionMaterializationRequestCM.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Derive data protection populated virtual status while host pvc waits with stale fake volume name", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvcWithFakeVolumeName.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPendingPvcWithVolumeNameBoundStatus.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionPopulatedPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvcWithFakeVolumeNameAndUID.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("ConfigMap"): {dataProtectionMaterializationRequestCM.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvcWithFakeVolumeName.DeepCopy(), + dataProtectionHostPendingPvcWithFakeVolumeName.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Do not derive data protection pvc volume name from stale same-name claim ref uid", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPendingPvc.DeepCopy(), + dataProtectionStaleUIDPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionStaleUIDPendingPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionStaleUIDPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvcWithUID.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + result, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPendingPvc.DeepCopy(), + dataProtectionBackupPendingPvc.DeepCopy(), + )) + assert.NilError(t, err) + assert.Check(t, !result.Requeue) + }, + }, + { + Name: "Do not preserve data protection populated virtual status for stale same-name claim ref uid", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPvc.DeepCopy(), + dataProtectionStaleUIDPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionStaleUIDPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {dataProtectionStaleUIDPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionHostPendingPvcWithUID.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "Recreate pvc if volume name is different", + InitialVirtualState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: basePvc.ObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "test", + }, + }, + }, + InitialPhysicalState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: pObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "test2", + }, + }, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: basePvc.ObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "test2", + }, + }, + }, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: pObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "test2", + }, + }, + }, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + vPVC := &corev1.PersistentVolumeClaim{} + err := syncCtx.VirtualClient.Get(syncCtx, types.NamespacedName{ + Namespace: basePvc.Namespace, + Name: basePvc.Name, + }, vPVC) + assert.NilError(t, err) + + pPVC := &corev1.PersistentVolumeClaim{} err = syncCtx.HostClient.Get(syncCtx, types.NamespacedName{ Namespace: pObjectMeta.Namespace, Name: pObjectMeta.Name, @@ -356,3 +1054,341 @@ func TestSync(t *testing.T) { }, }) } + +func TestSync_ExternalPopulatorStatusNotOverwritten(t *testing.T) { + vObjectMeta := metav1.ObjectMeta{ + Name: "testpvc", + Namespace: "testns", + } + pObjectMeta := metav1.ObjectMeta{ + Name: translate.Default.HostName(nil, "testpvc", "testns").Name, + Namespace: "test", + Annotations: map[string]string{ + translate.NameAnnotation: vObjectMeta.Name, + translate.NamespaceAnnotation: vObjectMeta.Namespace, + translate.UIDAnnotation: "", + translate.KindAnnotation: corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim").String(), + translate.HostNamespaceAnnotation: "test", + translate.HostNameAnnotation: translate.Default.HostName(nil, "testpvc", "testns").Name, + }, + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + translate.NamespaceLabel: vObjectMeta.Namespace, + }, + } + apiGroup := "dataprotection.kubeblocks.io" + + syncertesting.RunTestsWithContext(t, func(vConfig *config.VirtualClusterConfig, pClient *testingutil.FakeIndexClient, vClient *testingutil.FakeIndexClient) *synccontext.RegisterContext { + ctx := syncertesting.NewFakeRegisterContext(vConfig, pClient, vClient) + ctx.Config.Sync.ToHost.StorageClasses.Enabled = false + return ctx + }, []*syncertesting.SyncTest{ + { + Name: "External populator PVC keeps virtual status on sync", + InitialVirtualState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: vObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "Backup", + Name: "my-backup", + }, + VolumeName: "pvc-restored-vol", + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("10Gi"), + }, + }, + }, + }, + InitialPhysicalState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: pObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "Backup", + Name: "my-backup", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + }, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: vObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "Backup", + Name: "my-backup", + }, + VolumeName: "pvc-restored-vol", + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("10Gi"), + }, + }, + }, + }, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: pObjectMeta, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "Backup", + Name: "my-backup", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + }, + }, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + + vPVC := &corev1.PersistentVolumeClaim{} + err := syncCtx.VirtualClient.Get(syncCtx, types.NamespacedName{ + Namespace: vObjectMeta.Namespace, + Name: vObjectMeta.Name, + }, vPVC) + assert.NilError(t, err) + + pPVC := &corev1.PersistentVolumeClaim{} + err = syncCtx.HostClient.Get(syncCtx, types.NamespacedName{ + Namespace: pObjectMeta.Namespace, + Name: pObjectMeta.Name, + }, pPVC) + assert.NilError(t, err) + + _, err = syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + pPVC.DeepCopy(), + pPVC.DeepCopy(), + vPVC.DeepCopy(), + vPVC.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + { + Name: "VolumeSnapshot PVC still gets host status overwrite", + InitialVirtualState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snapshot-pvc", + Namespace: "testns", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: func() *string { s := "snapshot.storage.k8s.io"; return &s }(), + Kind: "VolumeSnapshot", + Name: "my-snapshot", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + }, + }, + InitialPhysicalState: []runtime.Object{ + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: translate.Default.HostName(nil, "snapshot-pvc", "testns").Name, + Namespace: "test", + Annotations: map[string]string{ + translate.NameAnnotation: "snapshot-pvc", + translate.NamespaceAnnotation: "testns", + translate.UIDAnnotation: "", + translate.KindAnnotation: corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim").String(), + translate.HostNamespaceAnnotation: "test", + translate.HostNameAnnotation: translate.Default.HostName(nil, "snapshot-pvc", "testns").Name, + }, + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + translate.NamespaceLabel: "testns", + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: func() *string { s := "snapshot.storage.k8s.io"; return &s }(), + Kind: "VolumeSnapshot", + Name: "my-snapshot", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + }, + }, + }, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "snapshot-pvc", + Namespace: "testns", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: func() *string { s := "snapshot.storage.k8s.io"; return &s }(), + Kind: "VolumeSnapshot", + Name: "my-snapshot", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + }, + }, + }, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): { + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: translate.Default.HostName(nil, "snapshot-pvc", "testns").Name, + Namespace: "test", + Annotations: map[string]string{ + translate.NameAnnotation: "snapshot-pvc", + translate.NamespaceAnnotation: "testns", + translate.UIDAnnotation: "", + translate.KindAnnotation: corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim").String(), + translate.HostNamespaceAnnotation: "test", + translate.HostNameAnnotation: translate.Default.HostName(nil, "snapshot-pvc", "testns").Name, + }, + Labels: map[string]string{ + translate.MarkerLabel: translate.VClusterName, + translate.NamespaceLabel: "testns", + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: func() *string { s := "snapshot.storage.k8s.io"; return &s }(), + Kind: "VolumeSnapshot", + Name: "my-snapshot", + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimBound, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + }, + }, + }, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + + vPVC := &corev1.PersistentVolumeClaim{} + err := syncCtx.VirtualClient.Get(syncCtx, types.NamespacedName{ + Namespace: "testns", + Name: "snapshot-pvc", + }, vPVC) + assert.NilError(t, err) + + pPVC := &corev1.PersistentVolumeClaim{} + err = syncCtx.HostClient.Get(syncCtx, types.NamespacedName{ + Namespace: "test", + Name: translate.Default.HostName(nil, "snapshot-pvc", "testns").Name, + }, pPVC) + assert.NilError(t, err) + + _, err = syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + pPVC.DeepCopy(), + pPVC.DeepCopy(), + vPVC.DeepCopy(), + vPVC.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, + }) +} + +func TestHasExternalPopulatorDataSource(t *testing.T) { + apiGroup := "dataprotection.kubeblocks.io" + snapshotGroup := "snapshot.storage.k8s.io" + + tests := []struct { + name string + pvc *corev1.PersistentVolumeClaim + expected bool + }{ + { + name: "nil dataSourceRef", + pvc: &corev1.PersistentVolumeClaim{}, + expected: false, + }, + { + name: "VolumeSnapshot kind", + pvc: &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &snapshotGroup, + Kind: "VolumeSnapshot", + Name: "snap", + }, + }, + }, + expected: false, + }, + { + name: "PersistentVolumeClaim kind", + pvc: &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + Kind: "PersistentVolumeClaim", + Name: "source-pvc", + }, + }, + }, + expected: false, + }, + { + name: "Backup kind", + pvc: &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "Backup", + Name: "my-backup", + }, + }, + }, + expected: true, + }, + { + name: "custom external populator kind", + pvc: &corev1.PersistentVolumeClaim{ + Spec: corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &apiGroup, + Kind: "CustomPopulator", + Name: "custom", + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, hasExternalPopulatorDataSource(tt.pvc), tt.expected) + }) + } +} diff --git a/pkg/controllers/resources/persistentvolumeclaims/translate.go b/pkg/controllers/resources/persistentvolumeclaims/translate.go index 01f2c6355b..a6f7b62dbd 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/translate.go +++ b/pkg/controllers/resources/persistentvolumeclaims/translate.go @@ -108,4 +108,10 @@ func (s *persistentVolumeClaimSyncer) translateUpdateBackwards(pObj, vObj *corev } vObj.Annotations[storageProvisionerAnnotation] = pObj.Annotations[storageProvisionerAnnotation] } + if vObj.Annotations[selectedNodeAnnotation] != pObj.Annotations[selectedNodeAnnotation] { + if vObj.Annotations == nil { + vObj.Annotations = map[string]string{} + } + vObj.Annotations[selectedNodeAnnotation] = pObj.Annotations[selectedNodeAnnotation] + } } diff --git a/pkg/controllers/resources/persistentvolumes/syncer_test.go b/pkg/controllers/resources/persistentvolumes/syncer_test.go index d2bee695e7..bc8d52c700 100644 --- a/pkg/controllers/resources/persistentvolumes/syncer_test.go +++ b/pkg/controllers/resources/persistentvolumes/syncer_test.go @@ -518,3 +518,61 @@ func TestSync(t *testing.T) { }) } } + +func TestTranslateUpdateBackwards_ClaimRefResourceVersionPreserved(t *testing.T) { + createContext := func(vConfig *config.VirtualClusterConfig, pClient *testingutil.FakeIndexClient, vClient *testingutil.FakeIndexClient) *synccontext.RegisterContext { + vConfig.Sync.ToHost.PersistentVolumes.Enabled = true + return syncertesting.NewFakeRegisterContext(vConfig, pClient, vClient) + } + + test := &syncertesting.SyncTest{ + Name: "ClaimRef ResourceVersion preserved on PVC update", + Sync: func(ctx *synccontext.RegisterContext) { + syncContext, syncer := newFakeSyncer(t, ctx) + + bindTimeRV := "100" + currentPvcRV := "200" + + vPv := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "testpv"}, + Spec: corev1.PersistentVolumeSpec{ + ClaimRef: &corev1.ObjectReference{ + Name: "testpvc", + Namespace: "test", + UID: "pvc-uid-1", + ResourceVersion: bindTimeRV, + }, + }, + } + pPv := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "testpv"}, + Spec: corev1.PersistentVolumeSpec{ + ClaimRef: &corev1.ObjectReference{ + Name: translate.Default.HostName(nil, "testpvc", "test").Name, + Namespace: "test", + UID: "host-pvc-uid", + ResourceVersion: "host-rv", + }, + }, + } + vPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testpvc", + Namespace: "test", + UID: "pvc-uid-1", + ResourceVersion: currentPvcRV, + }, + } + + err := syncer.translateUpdateBackwards(syncContext, vPv, pPv, vPvc) + assert.NilError(t, err) + + assert.Equal(t, vPv.Spec.ClaimRef.ResourceVersion, bindTimeRV) + assert.Equal(t, vPv.Spec.ClaimRef.Name, "testpvc") + assert.Equal(t, vPv.Spec.ClaimRef.Namespace, "test") + assert.Equal(t, string(vPv.Spec.ClaimRef.UID), "pvc-uid-1") + }, + } + + test.Run(t, createContext) +} diff --git a/pkg/controllers/resources/persistentvolumes/translate.go b/pkg/controllers/resources/persistentvolumes/translate.go index 675f45bb70..5fbc39fd50 100644 --- a/pkg/controllers/resources/persistentvolumes/translate.go +++ b/pkg/controllers/resources/persistentvolumes/translate.go @@ -54,10 +54,14 @@ func (s *persistentVolumeSyncer) translateUpdateBackwards(ctx *synccontext.SyncC translatedSpec.ClaimRef = &corev1.ObjectReference{} } - translatedSpec.ClaimRef.ResourceVersion = vPvc.ResourceVersion translatedSpec.ClaimRef.UID = vPvc.UID translatedSpec.ClaimRef.Name = vPvc.Name translatedSpec.ClaimRef.Namespace = vPvc.Namespace + if vPv.Spec.ClaimRef != nil { + translatedSpec.ClaimRef.ResourceVersion = vPv.Spec.ClaimRef.ResourceVersion + } else { + translatedSpec.ClaimRef.ResourceVersion = "" + } if vPvc.Spec.StorageClassName != nil { translatedSpec.StorageClassName = *vPvc.Spec.StorageClassName } From a2311179dec713c2fe14f0961e8af43d49f5c681 Mon Sep 17 00:00:00 2001 From: Gina Date: Wed, 24 Jun 2026 08:42:21 +0800 Subject: [PATCH 2/3] fix(pvc): catch processing no-data restore pvc --- .../persistentvolumeclaims/syncer.go | 29 ++++++++- .../persistentvolumeclaims/syncer_test.go | 62 ++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer.go b/pkg/controllers/resources/persistentvolumeclaims/syncer.go index 5e11784b00..10bbbeeaa6 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "strings" "time" storagev1 "k8s.io/api/storage/v1" @@ -55,7 +56,11 @@ const ( dataProtectionMaterializationRequestPrefix = "dp-host-materialization-" dataProtectionMaterializationStatePending = "pending" + dataProtectionRestoreConditionType = corev1.PersistentVolumeClaimConditionType("Restore") + dataProtectionPopulateConditionType = corev1.PersistentVolumeClaimConditionType("Populating") dataProtectionRestoreConditionReasonProvisioned = "Provisioned" + dataProtectionRestoreConditionReasonProcessing = "Processing" + dataProtectionNoDataRestoreMessage = "Provisioning PVC without data restore" ) func New(ctx *synccontext.RegisterContext) (syncertypes.Object, error) { @@ -766,7 +771,7 @@ func isHostPVCWaitingForVolume(pvc *corev1.PersistentVolumeClaim) bool { func isDataProtectionRestoreProvisionedWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { for _, condition := range pvc.Status.Conditions { - if condition.Type == corev1.PersistentVolumeClaimConditionType("Restore") && + if condition.Type == dataProtectionRestoreConditionType && condition.Status == corev1.ConditionTrue && condition.Reason == dataProtectionRestoreConditionReasonProvisioned { return true @@ -776,6 +781,25 @@ func isDataProtectionRestoreProvisionedWithoutDataRestore(pvc *corev1.Persistent return false } +func isDataProtectionRestoreProvisioningWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { + for _, condition := range pvc.Status.Conditions { + if condition.Type != dataProtectionRestoreConditionType && + condition.Type != dataProtectionPopulateConditionType { + continue + } + + if condition.Status == corev1.ConditionFalse || + condition.Reason != dataProtectionRestoreConditionReasonProcessing || + !strings.Contains(condition.Message, dataProtectionNoDataRestoreMessage) { + continue + } + + return true + } + + return false +} + func clearDataProtectionHostDataSource(pvc *corev1.PersistentVolumeClaim) { pvc.Spec.DataSource = nil pvc.Spec.DataSourceRef = nil @@ -804,7 +828,8 @@ func (s *persistentVolumeClaimSyncer) isDataProtectionNoDataRestorePVC(ctx *sync return false, nil } - return isDataProtectionRestoreProvisionedWithoutDataRestore(vObj), nil + return isDataProtectionRestoreProvisionedWithoutDataRestore(vObj) || + isDataProtectionRestoreProvisioningWithoutDataRestore(vObj), nil } func deleteDataProtectionNoDataRestoreHostPVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (ctrl.Result, error) { diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go index 434bfae668..a976f4b411 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go @@ -223,14 +223,32 @@ func TestSync(t *testing.T) { dataProtectionNoDataRestorePvc := dataProtectionBackupPvc.DeepCopy() dataProtectionNoDataRestorePvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ { - Type: corev1.PersistentVolumeClaimConditionType("Restore"), + Type: dataProtectionRestoreConditionType, Status: corev1.ConditionTrue, Reason: dataProtectionRestoreConditionReasonProvisioned, }, } + dataProtectionNoDataRestoreProcessingPvc := dataProtectionBackupPendingPvc.DeepCopy() + dataProtectionNoDataRestoreProcessingPvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ + { + Type: dataProtectionPopulateConditionType, + Status: corev1.ConditionTrue, + Reason: dataProtectionRestoreConditionReasonProcessing, + Message: dataProtectionNoDataRestoreMessage, + }, + { + Type: dataProtectionRestoreConditionType, + Status: corev1.ConditionUnknown, + Reason: dataProtectionRestoreConditionReasonProcessing, + Message: dataProtectionNoDataRestoreMessage, + }, + } dataProtectionNoDataHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() dataProtectionNoDataHostPvc.Spec = corev1.PersistentVolumeClaimSpec{} dataProtectionNoDataHostPvc.Status = dataProtectionNoDataRestorePvc.Status + dataProtectionNoDataHostProcessingPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionNoDataHostProcessingPvc.Spec = corev1.PersistentVolumeClaimSpec{} + dataProtectionNoDataHostProcessingPvc.Status = dataProtectionNoDataRestoreProcessingPvc.Status dataProtectionNoDataHostPendingWithBackupSource := dataProtectionHostPendingPvcWithUID.DeepCopy() dataProtectionNoDataHostPendingWithBackupSource.Spec = corev1.PersistentVolumeClaimSpec{ DataSource: &corev1.TypedLocalObjectReference{ @@ -245,6 +263,8 @@ func TestSync(t *testing.T) { }, } dataProtectionNoDataHostPendingWithBackupSource.ResourceVersion = "1" + dataProtectionNoDataHostProcessingWithBackupSource := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostProcessingWithBackupSource.Status = dataProtectionNoDataRestoreProcessingPvc.Status dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.Spec.VolumeName = dataProtectionPopulatedPV.Name dataProtectionDataRestoreHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() @@ -404,6 +424,23 @@ func TestSync(t *testing.T) { assert.NilError(t, err) }, }, + { + Name: "Create data protection no-data restore processing forward without host backup data source", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestoreProcessingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestoreProcessingPvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataHostProcessingPvc.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).SyncToHost(syncCtx, synccontext.NewSyncToHostEvent(dataProtectionNoDataRestoreProcessingPvc.DeepCopy())) + assert.NilError(t, err) + }, + }, { Name: "Recreate data protection no-data host pvc without deleting virtual after stale host pvc was deleted", InitialVirtualState: []runtime.Object{dataProtectionNoDataRestorePvc.DeepCopy()}, @@ -624,6 +661,29 @@ func TestSync(t *testing.T) { assert.NilError(t, err) }, }, + { + Name: "Delete existing host backup data source pvc while no-data restore is processing", + InitialVirtualState: []runtime.Object{dataProtectionNoDataRestoreProcessingPvc.DeepCopy()}, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostProcessingWithBackupSource.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestoreProcessingPvc.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostProcessingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostProcessingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestoreProcessingPvc.DeepCopy(), + dataProtectionNoDataRestoreProcessingPvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, { Name: "Preserve existing data restore host backup data source pvc with stale fake volume name after populated pv exists", InitialVirtualState: []runtime.Object{ From d666fcdb690b9b065c6d159968903fed270b5710 Mon Sep 17 00:00:00 2001 From: Gina Date: Wed, 24 Jun 2026 09:42:14 +0800 Subject: [PATCH 3/3] fix(pvc): generalize external populator binding repair --- .../persistentvolumeclaims/syncer.go | 215 +++++++++--------- .../persistentvolumeclaims/syncer_test.go | 70 ++++-- 2 files changed, 160 insertions(+), 125 deletions(-) diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer.go b/pkg/controllers/resources/persistentvolumeclaims/syncer.go index 10bbbeeaa6..466887f476 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer.go @@ -48,19 +48,17 @@ const ( storageProvisionerAnnotation = "volume.beta.kubernetes.io/storage-provisioner" selectedNodeAnnotation = "volume.kubernetes.io/selected-node" - dataProtectionAPIGroup = "dataprotection.kubeblocks.io" - dataProtectionBackupKind = "Backup" - dataProtectionPopulateFromAnnotation = "dataprotection.kubeblocks.io/populate-from" - - dataProtectionMaterializationRequestLabel = "vcluster.loft.sh/dataprotection-materialization-request" - dataProtectionMaterializationRequestPrefix = "dp-host-materialization-" - dataProtectionMaterializationStatePending = "pending" - - dataProtectionRestoreConditionType = corev1.PersistentVolumeClaimConditionType("Restore") - dataProtectionPopulateConditionType = corev1.PersistentVolumeClaimConditionType("Populating") - dataProtectionRestoreConditionReasonProvisioned = "Provisioned" - dataProtectionRestoreConditionReasonProcessing = "Processing" - dataProtectionNoDataRestoreMessage = "Provisioning PVC without data restore" + legacyDataProtectionPopulateFromAnnotation = "dataprotection.kubeblocks.io/populate-from" + + externalPopulatorMaterializationRequestLabel = "vcluster.loft.sh/external-populator-materialization-request" + externalPopulatorMaterializationRequestPrefix = "external-populator-materialization-" + externalPopulatorMaterializationStatePending = "pending" + + externalPopulatorRestoreConditionType = corev1.PersistentVolumeClaimConditionType("Restore") + externalPopulatorPopulateConditionType = corev1.PersistentVolumeClaimConditionType("Populating") + externalPopulatorRestoreConditionReasonProvisioned = "Provisioned" + externalPopulatorRestoreConditionReasonProcessing = "Processing" + externalPopulatorNoDataRestoreMessage = "Provisioning PVC without data restore" ) func New(ctx *synccontext.RegisterContext) (syncertypes.Object, error) { @@ -124,13 +122,13 @@ func (s *persistentVolumeClaimSyncer) SyncToHost(ctx *synccontext.SyncContext, e return ctrl.Result{}, nil } - preserveDeletingHostPVC, err := s.shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) + preserveDeletingHostPVC, err := s.shouldPreserveExternalPopulatorNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) if err != nil { return ctrl.Result{}, err } if event.HostOld != nil && preserveDeletingHostPVC && event.Virtual.DeletionTimestamp == nil { // The host PVC was intentionally deleted so it can be recreated without - // the Backup dataSource. Keep the virtual restore PVC and continue into + // the external dataSource. Keep the virtual restore PVC and continue into // the create path below. } else if event.HostOld != nil || event.Virtual.DeletionTimestamp != nil { return patcher.DeleteVirtualObjectWithOptions(ctx, event.Virtual, event.HostOld, "host object was deleted", &client.DeleteOptions{ @@ -138,7 +136,7 @@ func (s *persistentVolumeClaimSyncer) SyncToHost(ctx *synccontext.SyncContext, e }) } - pObj, handled, err := s.translateDataProtectionBackupToHost(ctx, event.Virtual) + pObj, handled, err := s.translateExternalPopulatorNoDataRestoreToHost(ctx, event.Virtual) if err != nil { s.EventRecorder().Eventf( event.Virtual, @@ -202,7 +200,7 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * // if pvs are deleted check the corresponding pvc is deleted as well if event.Host.DeletionTimestamp != nil { - preserveDeletingHostPVC, err := s.shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) + preserveDeletingHostPVC, err := s.shouldPreserveExternalPopulatorNoDataRestorePVCWhileHostDeleting(ctx, event.Virtual) if err != nil { return ctrl.Result{}, err } @@ -222,12 +220,12 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * Preconditions: metav1.NewUIDPreconditions(string(event.Host.UID)), }) } - recreateHostPVC, err := s.shouldRecreateDataProtectionHostNoDataRestorePVC(ctx, event.Host, event.Virtual) + recreateHostPVC, err := s.shouldRecreateExternalPopulatorHostNoDataRestorePVC(ctx, event.Host, event.Virtual) if err != nil { return ctrl.Result{}, err } if recreateHostPVC { - return deleteDataProtectionNoDataRestoreHostPVC(ctx, event.Host, event.Virtual) + return deleteExternalPopulatorNoDataRestoreHostPVC(ctx, event.Host, event.Virtual) } // make sure the persistent volume is synced / faked @@ -239,7 +237,7 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * return ctrl.Result{Requeue: true}, nil } } else { - requeue, err := s.ensureDataProtectionPopulatedPersistentVolumeName(ctx, event.Host, event.Virtual, ctx.Log) + requeue, err := s.ensureExternalPopulatorPersistentVolumeName(ctx, event.Host, event.Virtual, ctx.Log) if err != nil { return ctrl.Result{}, err } else if requeue { @@ -280,16 +278,16 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * s.translateUpdateBackwards(event.Host, event.Virtual) // copy host status - vPV, preserveVirtualStatus, err := s.dataProtectionPopulatedPersistentVolume(ctx, event.Host, event.Virtual) + vPV, preserveVirtualStatus, err := s.externalPopulatorPersistentVolume(ctx, event.Host, event.Virtual) if err != nil { return ctrl.Result{}, err } if preserveVirtualStatus { - err = s.ensureDataProtectionHostMaterialization(ctx, event.Host, event.Virtual, vPV) + err = s.ensureExternalPopulatorHostMaterialization(ctx, event.Host, event.Virtual, vPV) if err != nil { return ctrl.Result{}, err } - ensureDataProtectionVirtualPopulateStatus(event.Virtual, vPV) + ensureExternalPopulatorVirtualPopulateStatus(event.Virtual, vPV) } else { preserveExternalPopulatorStatus, err := s.shouldPreserveExternalPopulatorVirtualStatus(ctx, event.Host, event.Virtual) if err != nil { @@ -325,8 +323,8 @@ func (s *persistentVolumeClaimSyncer) SyncToVirtual(ctx *synccontext.SyncContext return patcher.CreateVirtualObject(ctx, event.Host, vPvc, s.EventRecorder(), true) } -func (s *persistentVolumeClaimSyncer) translateDataProtectionBackupToHost(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, bool, error) { - noDataRestore, err := s.isDataProtectionNoDataRestorePVC(ctx, vObj) +func (s *persistentVolumeClaimSyncer) translateExternalPopulatorNoDataRestoreToHost(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, bool, error) { + noDataRestore, err := s.isExternalPopulatorNoDataRestorePVC(ctx, vObj) if err != nil { return nil, true, err } @@ -339,7 +337,7 @@ func (s *persistentVolumeClaimSyncer) translateDataProtectionBackupToHost(ctx *s return nil, true, err } - clearDataProtectionHostDataSource(pObj) + clearExternalPopulatorHostDataSource(pObj) pObj.Spec.VolumeName = "" return pObj, true, nil } @@ -405,17 +403,17 @@ func (s *persistentVolumeClaimSyncer) ensurePersistentVolume(ctx *synccontext.Sy return false, nil } -func (s *persistentVolumeClaimSyncer) ensureDataProtectionPopulatedPersistentVolumeName(ctx *synccontext.SyncContext, pObj *corev1.PersistentVolumeClaim, vObj *corev1.PersistentVolumeClaim, log loghelper.Logger) (bool, error) { - if vObj.Spec.VolumeName != "" || !isDataProtectionBackupPVC(vObj) || !isHostPVCWaitingForVolume(pObj) { +func (s *persistentVolumeClaimSyncer) ensureExternalPopulatorPersistentVolumeName(ctx *synccontext.SyncContext, pObj *corev1.PersistentVolumeClaim, vObj *corev1.PersistentVolumeClaim, log loghelper.Logger) (bool, error) { + if vObj.Spec.VolumeName != "" || !isExternalPopulatorPVC(vObj) || !isHostPVCWaitingForVolume(pObj) { return false, nil } - vPV, ok, err := s.findDataProtectionPopulatedPersistentVolumeByClaimRef(ctx, vObj) + vPV, ok, err := s.findExternalPopulatorPersistentVolumeByClaimRef(ctx, vObj) if err != nil || !ok { return false, err } - log.Infof("update virtual data protection pvc %s/%s volume name to populated pv %s", vObj.Namespace, vObj.Name, vPV.Name) + log.Infof("update virtual external populator pvc %s/%s volume name to populated pv %s", vObj.Namespace, vObj.Name, vPV.Name) vObj.Spec.VolumeName = vPV.Name err = ctx.VirtualClient.Update(ctx, vObj) if err != nil { @@ -427,7 +425,7 @@ func (s *persistentVolumeClaimSyncer) ensureDataProtectionPopulatedPersistentVol return true, nil } -func (s *persistentVolumeClaimSyncer) findDataProtectionPopulatedPersistentVolumeByClaimRef(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { +func (s *persistentVolumeClaimSyncer) findExternalPopulatorPersistentVolumeByClaimRef(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { vPVs := &corev1.PersistentVolumeList{} err := ctx.VirtualClient.List(ctx.Context, vPVs) if err != nil { @@ -437,11 +435,11 @@ func (s *persistentVolumeClaimSyncer) findDataProtectionPopulatedPersistentVolum var match *corev1.PersistentVolume for i := range vPVs.Items { vPV := &vPVs.Items[i] - if !isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, true) { + if !isExternalPopulatorPersistentVolumeForPVC(vPV, vObj, true) { continue } if match != nil && match.Name != vPV.Name { - return nil, false, fmt.Errorf("multiple data protection populated persistent volumes match pvc %s/%s", vObj.Namespace, vObj.Name) + return nil, false, fmt.Errorf("multiple external populator populated persistent volumes match pvc %s/%s", vObj.Namespace, vObj.Name) } match = vPV.DeepCopy() } @@ -453,28 +451,28 @@ func (s *persistentVolumeClaimSyncer) findDataProtectionPopulatedPersistentVolum return match, true, nil } -func (s *persistentVolumeClaimSyncer) ensureDataProtectionHostMaterialization(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { - hostPVName := s.dataProtectionHostPersistentVolumeName(ctx, vPV) +func (s *persistentVolumeClaimSyncer) ensureExternalPopulatorHostMaterialization(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { + hostPVName := s.externalPopulatorHostPersistentVolumeName(ctx, vPV) hostPV := &corev1.PersistentVolume{} err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{Name: hostPVName}, hostPV) if err != nil { if kerrors.IsNotFound(err) { - return s.upsertDataProtectionMaterializationRequest(ctx, pObj, vObj, vPV) + return s.upsertExternalPopulatorMaterializationRequest(ctx, pObj, vObj, vPV) } return err } - helperPVC, helperFound, err := s.findDataProtectionPopulateHelperPVC(ctx, vObj, vPV) + helperPVC, helperFound, err := s.findExternalPopulatorHelperPVC(ctx, vObj, vPV) if err != nil { return err } pObj.Spec.VolumeName = hostPVName - return s.ensureDataProtectionHostPVClaimRef(ctx, hostPVName, pObj, helperPVC, helperFound) + return s.ensureExternalPopulatorHostPVClaimRef(ctx, hostPVName, pObj, helperPVC, helperFound) } -func (s *persistentVolumeClaimSyncer) upsertDataProtectionMaterializationRequest(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { - desired := dataProtectionMaterializationRequest(ctx.Config.HostNamespace, pObj, vObj, vPV) +func (s *persistentVolumeClaimSyncer) upsertExternalPopulatorMaterializationRequest(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) error { + desired := externalPopulatorMaterializationRequest(ctx.Config.HostNamespace, pObj, vObj, vPV) existing := &corev1.ConfigMap{} err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{ Namespace: desired.Namespace, @@ -498,7 +496,7 @@ func (s *persistentVolumeClaimSyncer) upsertDataProtectionMaterializationRequest return ctx.HostClient.Patch(ctx.Context, updated, client.MergeFrom(existing)) } -func (s *persistentVolumeClaimSyncer) dataProtectionHostPersistentVolumeName(ctx *synccontext.SyncContext, vPV *corev1.PersistentVolume) string { +func (s *persistentVolumeClaimSyncer) externalPopulatorHostPersistentVolumeName(ctx *synccontext.SyncContext, vPV *corev1.PersistentVolume) string { if s.useFakePersistentVolumes { return vPV.Name } @@ -506,7 +504,7 @@ func (s *persistentVolumeClaimSyncer) dataProtectionHostPersistentVolumeName(ctx return mappings.VirtualToHostName(ctx, vPV.Name, "", mappings.PersistentVolumes()) } -func (s *persistentVolumeClaimSyncer) findDataProtectionPopulateHelperPVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) (*corev1.PersistentVolumeClaim, bool, error) { +func (s *persistentVolumeClaimSyncer) findExternalPopulatorHelperPVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) (*corev1.PersistentVolumeClaim, bool, error) { pvcList := &corev1.PersistentVolumeClaimList{} err := ctx.VirtualClient.List(ctx.Context, pvcList, client.InNamespace(vObj.Namespace)) if err != nil { @@ -520,7 +518,7 @@ func (s *persistentVolumeClaimSyncer) findDataProtectionPopulateHelperPVC(ctx *s continue } if match != nil && match.Name != pvc.Name { - return nil, false, fmt.Errorf("multiple data protection helper persistent volume claims match pv %s for pvc %s/%s", vPV.Name, vObj.Namespace, vObj.Name) + return nil, false, fmt.Errorf("multiple external populator helper persistent volume claims match pv %s for pvc %s/%s", vPV.Name, vObj.Namespace, vObj.Name) } match = pvc.DeepCopy() } @@ -531,7 +529,7 @@ func (s *persistentVolumeClaimSyncer) findDataProtectionPopulateHelperPVC(ctx *s return match, true, nil } -func (s *persistentVolumeClaimSyncer) ensureDataProtectionHostPVClaimRef(ctx *synccontext.SyncContext, hostPVName string, pObj *corev1.PersistentVolumeClaim, helperPVC *corev1.PersistentVolumeClaim, helperFound bool) error { +func (s *persistentVolumeClaimSyncer) ensureExternalPopulatorHostPVClaimRef(ctx *synccontext.SyncContext, hostPVName string, pObj *corev1.PersistentVolumeClaim, helperPVC *corev1.PersistentVolumeClaim, helperFound bool) error { hostPV := &corev1.PersistentVolume{} err := ctx.HostClient.Get(ctx.Context, types.NamespacedName{Name: hostPVName}, hostPV) if err != nil { @@ -605,8 +603,8 @@ func claimRefReferencesPersistentVolumeClaim(ref *corev1.ObjectReference, pvc *c return ref.Namespace == pvc.Namespace && ref.Name == pvc.Name } -func (s *persistentVolumeClaimSyncer) dataProtectionPopulatedPersistentVolume(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { - if !isDataProtectionBackupPVC(vObj) || vObj.Spec.VolumeName == "" || !isHostPVCWaitingForVolume(pObj) { +func (s *persistentVolumeClaimSyncer) externalPopulatorPersistentVolume(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { + if !isExternalPopulatorPVC(vObj) || vObj.Spec.VolumeName == "" || !isHostPVCWaitingForVolume(pObj) { return nil, false, nil } @@ -619,10 +617,7 @@ func (s *persistentVolumeClaimSyncer) dataProtectionPopulatedPersistentVolume(ct return nil, false, err } - if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { - return nil, false, nil - } - if !isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, false) { + if !isExternalPopulatorPersistentVolumeForPVC(vPV, vObj, false) { return nil, false, nil } if !isVirtualPVCBound(vObj) && vPV.Status.Phase != corev1.VolumeBound { @@ -636,9 +631,6 @@ func (s *persistentVolumeClaimSyncer) shouldPreserveExternalPopulatorVirtualStat if !hasExternalPopulatorDataSource(vObj) { return false, nil } - if !isDataProtectionBackupPVC(vObj) { - return true, nil - } if !isHostPVCWaitingForVolume(pObj) { return false, nil } @@ -654,14 +646,10 @@ func (s *persistentVolumeClaimSyncer) shouldPreserveExternalPopulatorVirtualStat } return false, err } - if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { - return true, nil - } - - return isDataProtectionPopulatedPersistentVolumeForPVC(vPV, vObj, false), nil + return isExternalPopulatorPersistentVolumeForPVC(vPV, vObj, false), nil } -func ensureDataProtectionVirtualPopulateStatus(vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) { +func ensureExternalPopulatorVirtualPopulateStatus(vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) { if isVirtualPVCBound(vObj) { return } @@ -680,13 +668,10 @@ func ensureDataProtectionVirtualPopulateStatus(vObj *corev1.PersistentVolumeClai } } -func isDataProtectionPopulatedPersistentVolumeForPVC(vPV *corev1.PersistentVolume, vObj *corev1.PersistentVolumeClaim, requireBoundPV bool) bool { +func isExternalPopulatorPersistentVolumeForPVC(vPV *corev1.PersistentVolume, vObj *corev1.PersistentVolumeClaim, requireBoundPV bool) bool { if requireBoundPV && vPV.Status.Phase != corev1.VolumeBound { return false } - if vPV.Annotations[dataProtectionPopulateFromAnnotation] == "" { - return false - } if vPV.Spec.ClaimRef == nil || vPV.Spec.ClaimRef.Namespace != vObj.Namespace || vPV.Spec.ClaimRef.Name != vObj.Name { @@ -699,17 +684,17 @@ func isDataProtectionPopulatedPersistentVolumeForPVC(vPV *corev1.PersistentVolum return true } -func dataProtectionMaterializationRequest(hostNamespace string, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) *corev1.ConfigMap { +func externalPopulatorMaterializationRequest(hostNamespace string, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: hostNamespace, - Name: dataProtectionMaterializationRequestName(pObj), + Name: externalPopulatorMaterializationRequestName(pObj), Labels: map[string]string{ - dataProtectionMaterializationRequestLabel: "true", + externalPopulatorMaterializationRequestLabel: "true", }, }, Data: map[string]string{ - "state": dataProtectionMaterializationStatePending, + "state": externalPopulatorMaterializationStatePending, "hostPVCNamespace": pObj.Namespace, "hostPVCName": pObj.Name, "virtualPVCNamespace": vObj.Namespace, @@ -717,29 +702,25 @@ func dataProtectionMaterializationRequest(hostNamespace string, pObj, vObj *core "virtualPVCUID": string(vObj.UID), "virtualPVName": vPV.Name, "virtualPVUID": string(vPV.UID), - "backupName": vObj.Spec.DataSourceRef.Name, - "populateFrom": vPV.Annotations[dataProtectionPopulateFromAnnotation], + "dataSourceAPIGroup": externalPopulatorDataSourceAPIGroup(vObj.Spec.DataSourceRef), + "dataSourceKind": vObj.Spec.DataSourceRef.Kind, + "dataSourceName": vObj.Spec.DataSourceRef.Name, + "populateFrom": vPV.Annotations[legacyDataProtectionPopulateFromAnnotation], }, } } -func dataProtectionMaterializationRequestName(pObj *corev1.PersistentVolumeClaim) string { +func externalPopulatorMaterializationRequestName(pObj *corev1.PersistentVolumeClaim) string { sum := sha256.Sum256([]byte(pObj.Namespace + "/" + pObj.Name)) - return dataProtectionMaterializationRequestPrefix + hex.EncodeToString(sum[:])[:16] + return externalPopulatorMaterializationRequestPrefix + hex.EncodeToString(sum[:])[:16] } -func isDataProtectionBackupPVC(pvc *corev1.PersistentVolumeClaim) bool { - if pvc.Spec.DataSourceRef == nil || pvc.Spec.DataSourceRef.APIGroup == nil { - return false - } - - return *pvc.Spec.DataSourceRef.APIGroup == dataProtectionAPIGroup && - pvc.Spec.DataSourceRef.Kind == dataProtectionBackupKind && - pvc.Spec.DataSourceRef.Name != "" +func isExternalPopulatorPVC(pvc *corev1.PersistentVolumeClaim) bool { + return hasExternalPopulatorDataSource(pvc) } func hasExternalPopulatorDataSource(pvc *corev1.PersistentVolumeClaim) bool { - if pvc.Spec.DataSourceRef == nil { + if pvc.Spec.DataSourceRef == nil || pvc.Spec.DataSourceRef.Name == "" { return false } @@ -769,11 +750,11 @@ func isHostPVCWaitingForVolume(pvc *corev1.PersistentVolumeClaim) bool { return !ok || storage.IsZero() } -func isDataProtectionRestoreProvisionedWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { +func isExternalPopulatorRestoreProvisionedWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { for _, condition := range pvc.Status.Conditions { - if condition.Type == dataProtectionRestoreConditionType && + if condition.Type == externalPopulatorRestoreConditionType && condition.Status == corev1.ConditionTrue && - condition.Reason == dataProtectionRestoreConditionReasonProvisioned { + condition.Reason == externalPopulatorRestoreConditionReasonProvisioned { return true } } @@ -781,16 +762,16 @@ func isDataProtectionRestoreProvisionedWithoutDataRestore(pvc *corev1.Persistent return false } -func isDataProtectionRestoreProvisioningWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { +func isExternalPopulatorRestoreProvisioningWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { for _, condition := range pvc.Status.Conditions { - if condition.Type != dataProtectionRestoreConditionType && - condition.Type != dataProtectionPopulateConditionType { + if condition.Type != externalPopulatorRestoreConditionType && + condition.Type != externalPopulatorPopulateConditionType { continue } if condition.Status == corev1.ConditionFalse || - condition.Reason != dataProtectionRestoreConditionReasonProcessing || - !strings.Contains(condition.Message, dataProtectionNoDataRestoreMessage) { + condition.Reason != externalPopulatorRestoreConditionReasonProcessing || + !strings.Contains(condition.Message, externalPopulatorNoDataRestoreMessage) { continue } @@ -800,18 +781,18 @@ func isDataProtectionRestoreProvisioningWithoutDataRestore(pvc *corev1.Persisten return false } -func clearDataProtectionHostDataSource(pvc *corev1.PersistentVolumeClaim) { +func clearExternalPopulatorHostDataSource(pvc *corev1.PersistentVolumeClaim) { pvc.Spec.DataSource = nil pvc.Spec.DataSourceRef = nil } -func (s *persistentVolumeClaimSyncer) shouldRecreateDataProtectionHostNoDataRestorePVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (bool, error) { +func (s *persistentVolumeClaimSyncer) shouldRecreateExternalPopulatorHostNoDataRestorePVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (bool, error) { if !isHostPVCWaitingForVolume(pObj) || - (!isDataProtectionBackupDataSource(pObj.Spec.DataSource) && !isDataProtectionBackupDataSourceRef(pObj.Spec.DataSourceRef)) { + (!isExternalPopulatorDataSource(pObj.Spec.DataSource) && !isExternalPopulatorDataSourceRef(pObj.Spec.DataSourceRef)) { return false, nil } - noDataRestore, err := s.isDataProtectionNoDataRestorePVC(ctx, vObj) + noDataRestore, err := s.isExternalPopulatorNoDataRestorePVC(ctx, vObj) if err != nil || !noDataRestore { return false, err } @@ -819,21 +800,21 @@ func (s *persistentVolumeClaimSyncer) shouldRecreateDataProtectionHostNoDataRest return true, nil } -func (s *persistentVolumeClaimSyncer) shouldPreserveDataProtectionNoDataRestorePVCWhileHostDeleting(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { - return s.isDataProtectionNoDataRestorePVC(ctx, vObj) +func (s *persistentVolumeClaimSyncer) shouldPreserveExternalPopulatorNoDataRestorePVCWhileHostDeleting(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + return s.isExternalPopulatorNoDataRestorePVC(ctx, vObj) } -func (s *persistentVolumeClaimSyncer) isDataProtectionNoDataRestorePVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { - if !isDataProtectionBackupPVC(vObj) { +func (s *persistentVolumeClaimSyncer) isExternalPopulatorNoDataRestorePVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !isExternalPopulatorPVC(vObj) { return false, nil } - return isDataProtectionRestoreProvisionedWithoutDataRestore(vObj) || - isDataProtectionRestoreProvisioningWithoutDataRestore(vObj), nil + return isExternalPopulatorRestoreProvisionedWithoutDataRestore(vObj) || + isExternalPopulatorRestoreProvisioningWithoutDataRestore(vObj), nil } -func deleteDataProtectionNoDataRestoreHostPVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (ctrl.Result, error) { - result, err := patcher.DeleteHostObjectWithOptions(ctx, pObj, vObj, "data protection restore pvc was provisioned without data restore", &client.DeleteOptions{ +func deleteExternalPopulatorNoDataRestoreHostPVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (ctrl.Result, error) { + result, err := patcher.DeleteHostObjectWithOptions(ctx, pObj, vObj, "external populator restore pvc was provisioned without data restore", &client.DeleteOptions{ GracePeriodSeconds: &zero, Preconditions: hostDeletePreconditions(pObj), }) @@ -861,24 +842,38 @@ func hostDeletePreconditions(obj client.Object) *metav1.Preconditions { return preconditions } -func isDataProtectionBackupDataSource(ref *corev1.TypedLocalObjectReference) bool { - if ref == nil || ref.APIGroup == nil { +func isExternalPopulatorDataSource(ref *corev1.TypedLocalObjectReference) bool { + if ref == nil || ref.Name == "" { return false } - return *ref.APIGroup == dataProtectionAPIGroup && - ref.Kind == dataProtectionBackupKind && - ref.Name != "" + switch ref.Kind { + case "VolumeSnapshot", "PersistentVolumeClaim": + return false + default: + return true + } } -func isDataProtectionBackupDataSourceRef(ref *corev1.TypedObjectReference) bool { - if ref == nil || ref.APIGroup == nil { +func isExternalPopulatorDataSourceRef(ref *corev1.TypedObjectReference) bool { + if ref == nil || ref.Name == "" { return false } - return *ref.APIGroup == dataProtectionAPIGroup && - ref.Kind == dataProtectionBackupKind && - ref.Name != "" + switch ref.Kind { + case "VolumeSnapshot", "PersistentVolumeClaim": + return false + default: + return true + } +} + +func externalPopulatorDataSourceAPIGroup(ref *corev1.TypedObjectReference) string { + if ref == nil || ref.APIGroup == nil { + return "" + } + + return *ref.APIGroup } func (s *persistentVolumeClaimSyncer) isHostVolumeRestoreInProgress(ctx *synccontext.SyncContext, pObj types.NamespacedName) (bool, error) { diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go index a976f4b411..3e10957e70 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer_test.go @@ -142,7 +142,7 @@ func TestSync(t *testing.T) { ObjectMeta: vObjectMeta, Spec: backwardUpdateStatusPvc.Spec, } - dataProtectionGroup := dataProtectionAPIGroup + dataProtectionGroup := "dataprotection.kubeblocks.io" dataProtectionBackupPvc := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: vObjectMeta.Name, @@ -153,7 +153,7 @@ func TestSync(t *testing.T) { VolumeName: "restore-populated-pv", DataSourceRef: &corev1.TypedObjectReference{ APIGroup: &dataProtectionGroup, - Kind: dataProtectionBackupKind, + Kind: "Backup", Name: "backup-1", }, }, @@ -196,7 +196,7 @@ func TestSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "restore-populated-pv", Annotations: map[string]string{ - dataProtectionPopulateFromAnnotation: "backup-1", + legacyDataProtectionPopulateFromAnnotation: "backup-1", }, }, Spec: corev1.PersistentVolumeSpec{ @@ -219,28 +219,40 @@ func TestSync(t *testing.T) { dataProtectionStaleUIDPV := dataProtectionPopulatedPV.DeepCopy() dataProtectionStaleUIDPV.Spec.ClaimRef.UID = types.UID("stale-pvc-uid") dataProtectionStaleUIDPendingPvc := dataProtectionBackupPendingPvc.DeepCopy() - dataProtectionMaterializationRequestCM := dataProtectionMaterializationRequest("test", dataProtectionHostPendingPvc, dataProtectionBackupPvc, dataProtectionPopulatedPV) + dataProtectionMaterializationRequestCM := externalPopulatorMaterializationRequest("test", dataProtectionHostPendingPvc, dataProtectionBackupPvc, dataProtectionPopulatedPV) + customPopulatorGroup := "example.io" + customExternalPopulatorPvc := dataProtectionBackupPvc.DeepCopy() + customExternalPopulatorPvc.UID = types.UID("custom-target-pvc-uid") + customExternalPopulatorPvc.Spec.DataSourceRef.APIGroup = &customPopulatorGroup + customExternalPopulatorPvc.Spec.DataSourceRef.Kind = "Dataset" + customExternalPopulatorPvc.Spec.DataSourceRef.Name = "dataset-1" + customExternalPopulatorPV := dataProtectionPopulatedPV.DeepCopy() + customExternalPopulatorPV.Annotations = nil + customExternalPopulatorPV.Spec.ClaimRef.UID = customExternalPopulatorPvc.UID + customExternalPopulatorHostPendingPvcWithUID := dataProtectionHostPendingPvc.DeepCopy() + customExternalPopulatorHostPendingPvcWithUID.Annotations[translate.UIDAnnotation] = string(customExternalPopulatorPvc.UID) + customExternalPopulatorMaterializationRequestCM := externalPopulatorMaterializationRequest("test", dataProtectionHostPendingPvc, customExternalPopulatorPvc, customExternalPopulatorPV) dataProtectionNoDataRestorePvc := dataProtectionBackupPvc.DeepCopy() dataProtectionNoDataRestorePvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ { - Type: dataProtectionRestoreConditionType, + Type: externalPopulatorRestoreConditionType, Status: corev1.ConditionTrue, - Reason: dataProtectionRestoreConditionReasonProvisioned, + Reason: externalPopulatorRestoreConditionReasonProvisioned, }, } dataProtectionNoDataRestoreProcessingPvc := dataProtectionBackupPendingPvc.DeepCopy() dataProtectionNoDataRestoreProcessingPvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ { - Type: dataProtectionPopulateConditionType, + Type: externalPopulatorPopulateConditionType, Status: corev1.ConditionTrue, - Reason: dataProtectionRestoreConditionReasonProcessing, - Message: dataProtectionNoDataRestoreMessage, + Reason: externalPopulatorRestoreConditionReasonProcessing, + Message: externalPopulatorNoDataRestoreMessage, }, { - Type: dataProtectionRestoreConditionType, + Type: externalPopulatorRestoreConditionType, Status: corev1.ConditionUnknown, - Reason: dataProtectionRestoreConditionReasonProcessing, - Message: dataProtectionNoDataRestoreMessage, + Reason: externalPopulatorRestoreConditionReasonProcessing, + Message: externalPopulatorNoDataRestoreMessage, }, } dataProtectionNoDataHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() @@ -253,12 +265,12 @@ func TestSync(t *testing.T) { dataProtectionNoDataHostPendingWithBackupSource.Spec = corev1.PersistentVolumeClaimSpec{ DataSource: &corev1.TypedLocalObjectReference{ APIGroup: &dataProtectionGroup, - Kind: dataProtectionBackupKind, + Kind: "Backup", Name: "backup-1", }, DataSourceRef: &corev1.TypedObjectReference{ APIGroup: &dataProtectionGroup, - Kind: dataProtectionBackupKind, + Kind: "Backup", Name: "backup-1", }, } @@ -271,7 +283,7 @@ func TestSync(t *testing.T) { dataProtectionDataRestoreHostPvc.Spec = corev1.PersistentVolumeClaimSpec{ DataSourceRef: &corev1.TypedObjectReference{ APIGroup: &dataProtectionGroup, - Kind: dataProtectionBackupKind, + Kind: "Backup", Name: "backup-1", }, } @@ -839,6 +851,34 @@ func TestSync(t *testing.T) { assert.NilError(t, err) }, }, + { + Name: "Preserve custom external populator virtual status while host pvc waits for volume", + InitialVirtualState: []runtime.Object{ + customExternalPopulatorPvc.DeepCopy(), + customExternalPopulatorPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {customExternalPopulatorPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolume"): {customExternalPopulatorPV.DeepCopy()}, + }, + ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {customExternalPopulatorHostPendingPvcWithUID.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("ConfigMap"): {customExternalPopulatorMaterializationRequestCM.DeepCopy()}, + }, + Sync: func(ctx *synccontext.RegisterContext) { + syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) + syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionHostPendingPvc.DeepCopy(), + dataProtectionHostPendingPvc.DeepCopy(), + customExternalPopulatorPvc.DeepCopy(), + customExternalPopulatorPvc.DeepCopy(), + )) + assert.NilError(t, err) + }, + }, { Name: "Bridge data protection populated host pv from helper pvc to target pvc", InitialVirtualState: []runtime.Object{