diff --git a/pkg/controllers/resources/persistentvolumeclaims/syncer.go b/pkg/controllers/resources/persistentvolumeclaims/syncer.go index f7f310e46b..466887f476 100644 --- a/pkg/controllers/resources/persistentvolumeclaims/syncer.go +++ b/pkg/controllers/resources/persistentvolumeclaims/syncer.go @@ -1,12 +1,16 @@ package persistentvolumeclaims import ( + "crypto/sha256" + "encoding/hex" "fmt" + "strings" "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 +46,19 @@ 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" + + 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) { @@ -105,13 +122,42 @@ func (s *persistentVolumeClaimSyncer) SyncToHost(ctx *synccontext.SyncContext, e return ctrl.Result{}, nil } - if event.HostOld != nil || event.Virtual.DeletionTimestamp != nil { + 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 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{ GracePeriodSeconds: &zero, }) } - pObj, err := s.translate(ctx, event.Virtual) + pObj, handled, err := s.translateExternalPopulatorNoDataRestoreToHost(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 +178,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 +200,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.shouldPreserveExternalPopulatorNoDataRestorePVCWhileHostDeleting(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 +220,13 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * Preconditions: metav1.NewUIDPreconditions(string(event.Host.UID)), }) } + recreateHostPVC, err := s.shouldRecreateExternalPopulatorHostNoDataRestorePVC(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if recreateHostPVC { + return deleteExternalPopulatorNoDataRestoreHostPVC(ctx, event.Host, event.Virtual) + } // make sure the persistent volume is synced / faked if event.Host.Spec.VolumeName != "" { @@ -176,6 +236,13 @@ func (s *persistentVolumeClaimSyncer) Sync(ctx *synccontext.SyncContext, event * } else if requeue { return ctrl.Result{Requeue: true}, nil } + } else { + requeue, err := s.ensureExternalPopulatorPersistentVolumeName(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 +255,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 +278,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.externalPopulatorPersistentVolume(ctx, event.Host, event.Virtual) + if err != nil { + return ctrl.Result{}, err + } + if preserveVirtualStatus { + err = s.ensureExternalPopulatorHostMaterialization(ctx, event.Host, event.Virtual, vPV) + if err != nil { + return ctrl.Result{}, err + } + ensureExternalPopulatorVirtualPopulateStatus(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 +323,25 @@ func (s *persistentVolumeClaimSyncer) SyncToVirtual(ctx *synccontext.SyncContext return patcher.CreateVirtualObject(ctx, event.Host, vPvc, s.EventRecorder(), true) } +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 + } + if !noDataRestore { + return nil, false, nil + } + + pObj, err := s.translate(ctx, vObj) + if err != nil { + return nil, true, err + } + + clearExternalPopulatorHostDataSource(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 +393,489 @@ 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) 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.findExternalPopulatorPersistentVolumeByClaimRef(ctx, vObj) + if err != nil || !ok { + return false, err + } + + 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 { + 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) findExternalPopulatorPersistentVolumeByClaimRef(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 !isExternalPopulatorPersistentVolumeForPVC(vPV, vObj, true) { + continue + } + if match != nil && match.Name != vPV.Name { + return nil, false, fmt.Errorf("multiple external populator 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) 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.upsertExternalPopulatorMaterializationRequest(ctx, pObj, vObj, vPV) + } + return err + } + + helperPVC, helperFound, err := s.findExternalPopulatorHelperPVC(ctx, vObj, vPV) + if err != nil { + return err + } + + pObj.Spec.VolumeName = hostPVName + return s.ensureExternalPopulatorHostPVClaimRef(ctx, hostPVName, pObj, helperPVC, helperFound) +} + +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, + 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) externalPopulatorHostPersistentVolumeName(ctx *synccontext.SyncContext, vPV *corev1.PersistentVolume) string { + if s.useFakePersistentVolumes { + return vPV.Name + } + + return mappings.VirtualToHostName(ctx, vPV.Name, "", mappings.PersistentVolumes()) +} + +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 { + 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 external populator 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) 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 { + 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) externalPopulatorPersistentVolume(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, bool, error) { + if !isExternalPopulatorPVC(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 !isExternalPopulatorPersistentVolumeForPVC(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 !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 + } + return isExternalPopulatorPersistentVolumeForPVC(vPV, vObj, false), nil +} + +func ensureExternalPopulatorVirtualPopulateStatus(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 isExternalPopulatorPersistentVolumeForPVC(vPV *corev1.PersistentVolume, vObj *corev1.PersistentVolumeClaim, requireBoundPV bool) bool { + if requireBoundPV && vPV.Status.Phase != corev1.VolumeBound { + 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 externalPopulatorMaterializationRequest(hostNamespace string, pObj, vObj *corev1.PersistentVolumeClaim, vPV *corev1.PersistentVolume) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: hostNamespace, + Name: externalPopulatorMaterializationRequestName(pObj), + Labels: map[string]string{ + externalPopulatorMaterializationRequestLabel: "true", + }, + }, + Data: map[string]string{ + "state": externalPopulatorMaterializationStatePending, + "hostPVCNamespace": pObj.Namespace, + "hostPVCName": pObj.Name, + "virtualPVCNamespace": vObj.Namespace, + "virtualPVCName": vObj.Name, + "virtualPVCUID": string(vObj.UID), + "virtualPVName": vPV.Name, + "virtualPVUID": string(vPV.UID), + "dataSourceAPIGroup": externalPopulatorDataSourceAPIGroup(vObj.Spec.DataSourceRef), + "dataSourceKind": vObj.Spec.DataSourceRef.Kind, + "dataSourceName": vObj.Spec.DataSourceRef.Name, + "populateFrom": vPV.Annotations[legacyDataProtectionPopulateFromAnnotation], + }, + } +} + +func externalPopulatorMaterializationRequestName(pObj *corev1.PersistentVolumeClaim) string { + sum := sha256.Sum256([]byte(pObj.Namespace + "/" + pObj.Name)) + return externalPopulatorMaterializationRequestPrefix + hex.EncodeToString(sum[:])[:16] +} + +func isExternalPopulatorPVC(pvc *corev1.PersistentVolumeClaim) bool { + return hasExternalPopulatorDataSource(pvc) +} + +func hasExternalPopulatorDataSource(pvc *corev1.PersistentVolumeClaim) bool { + if pvc.Spec.DataSourceRef == nil || pvc.Spec.DataSourceRef.Name == "" { + 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 isExternalPopulatorRestoreProvisionedWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { + for _, condition := range pvc.Status.Conditions { + if condition.Type == externalPopulatorRestoreConditionType && + condition.Status == corev1.ConditionTrue && + condition.Reason == externalPopulatorRestoreConditionReasonProvisioned { + return true + } + } + + return false +} + +func isExternalPopulatorRestoreProvisioningWithoutDataRestore(pvc *corev1.PersistentVolumeClaim) bool { + for _, condition := range pvc.Status.Conditions { + if condition.Type != externalPopulatorRestoreConditionType && + condition.Type != externalPopulatorPopulateConditionType { + continue + } + + if condition.Status == corev1.ConditionFalse || + condition.Reason != externalPopulatorRestoreConditionReasonProcessing || + !strings.Contains(condition.Message, externalPopulatorNoDataRestoreMessage) { + continue + } + + return true + } + + return false +} + +func clearExternalPopulatorHostDataSource(pvc *corev1.PersistentVolumeClaim) { + pvc.Spec.DataSource = nil + pvc.Spec.DataSourceRef = nil +} + +func (s *persistentVolumeClaimSyncer) shouldRecreateExternalPopulatorHostNoDataRestorePVC(ctx *synccontext.SyncContext, pObj, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !isHostPVCWaitingForVolume(pObj) || + (!isExternalPopulatorDataSource(pObj.Spec.DataSource) && !isExternalPopulatorDataSourceRef(pObj.Spec.DataSourceRef)) { + return false, nil + } + + noDataRestore, err := s.isExternalPopulatorNoDataRestorePVC(ctx, vObj) + if err != nil || !noDataRestore { + return false, err + } + + return true, nil +} + +func (s *persistentVolumeClaimSyncer) shouldPreserveExternalPopulatorNoDataRestorePVCWhileHostDeleting(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + return s.isExternalPopulatorNoDataRestorePVC(ctx, vObj) +} + +func (s *persistentVolumeClaimSyncer) isExternalPopulatorNoDataRestorePVC(ctx *synccontext.SyncContext, vObj *corev1.PersistentVolumeClaim) (bool, error) { + if !isExternalPopulatorPVC(vObj) { + return false, nil + } + + return isExternalPopulatorRestoreProvisionedWithoutDataRestore(vObj) || + isExternalPopulatorRestoreProvisioningWithoutDataRestore(vObj), nil +} + +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), + }) + 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 isExternalPopulatorDataSource(ref *corev1.TypedLocalObjectReference) bool { + if ref == nil || ref.Name == "" { + return false + } + + switch ref.Kind { + case "VolumeSnapshot", "PersistentVolumeClaim": + return false + default: + return true + } +} + +func isExternalPopulatorDataSourceRef(ref *corev1.TypedObjectReference) bool { + if ref == nil || ref.Name == "" { + return false + } + + 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) { 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..3e10957e70 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,249 @@ func TestSync(t *testing.T) { Spec: backwardUpdateStatusPvc.Spec, Status: backwardUpdateStatusPvc.Status, } + backwardUpdateVolumeNameOnlyPvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: vObjectMeta, + Spec: backwardUpdateStatusPvc.Spec, + } + dataProtectionGroup := "dataprotection.kubeblocks.io" + 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: "Backup", + 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{ + legacyDataProtectionPopulateFromAnnotation: "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 := 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: externalPopulatorRestoreConditionType, + Status: corev1.ConditionTrue, + Reason: externalPopulatorRestoreConditionReasonProvisioned, + }, + } + dataProtectionNoDataRestoreProcessingPvc := dataProtectionBackupPendingPvc.DeepCopy() + dataProtectionNoDataRestoreProcessingPvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{ + { + Type: externalPopulatorPopulateConditionType, + Status: corev1.ConditionTrue, + Reason: externalPopulatorRestoreConditionReasonProcessing, + Message: externalPopulatorNoDataRestoreMessage, + }, + { + Type: externalPopulatorRestoreConditionType, + Status: corev1.ConditionUnknown, + Reason: externalPopulatorRestoreConditionReasonProcessing, + Message: externalPopulatorNoDataRestoreMessage, + }, + } + 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{ + APIGroup: &dataProtectionGroup, + Kind: "Backup", + Name: "backup-1", + }, + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: "Backup", + Name: "backup-1", + }, + } + dataProtectionNoDataHostPendingWithBackupSource.ResourceVersion = "1" + dataProtectionNoDataHostProcessingWithBackupSource := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostProcessingWithBackupSource.Status = dataProtectionNoDataRestoreProcessingPvc.Status + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName := dataProtectionNoDataHostPendingWithBackupSource.DeepCopy() + dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.Spec.VolumeName = dataProtectionPopulatedPV.Name + dataProtectionDataRestoreHostPvc := dataProtectionHostPendingPvcWithUID.DeepCopy() + dataProtectionDataRestoreHostPvc.Spec = corev1.PersistentVolumeClaimSpec{ + DataSourceRef: &corev1.TypedObjectReference{ + APIGroup: &dataProtectionGroup, + Kind: "Backup", + 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 +402,97 @@ 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: "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()}, + 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 +575,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,92 +651,844 @@ func TestSync(t *testing.T) { }, }, { - Name: "Update backwards new status", - InitialVirtualState: []runtime.Object{basePvc.DeepCopy()}, - InitialPhysicalState: []runtime.Object{backwardUpdateStatusPvc.DeepCopy()}, + 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"): {backwardUpdatedStatusPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionNoDataRestorePvc.DeepCopy()}, }, ExpectedPhysicalState: map[schema.GroupVersionKind][]runtime.Object{ - corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {backwardUpdateStatusPvc.DeepCopy()}, + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {}, }, Sync: func(ctx *synccontext.RegisterContext) { syncCtx, syncer := syncertesting.FakeStartSyncer(t, ctx, New) syncer.(*persistentVolumeClaimSyncer).useFakePersistentVolumes = true - pObjOld := backwardUpdateStatusPvc.DeepCopy() - pObj := backwardUpdateStatusPvc.DeepCopy() - vObjOld := basePvc.DeepCopy() - vObj := basePvc.DeepCopy() + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostPendingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + dataProtectionNoDataRestorePvc.DeepCopy(), + )) + 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( - pObjOld, - pObj, - vObjOld, - vObj, + dataProtectionNoDataHostProcessingWithBackupSource.DeepCopy(), + dataProtectionNoDataHostProcessingWithBackupSource.DeepCopy(), + dataProtectionNoDataRestoreProcessingPvc.DeepCopy(), + dataProtectionNoDataRestoreProcessingPvc.DeepCopy(), )) assert.NilError(t, err) }, }, { - Name: "Recreate pvc if volume name is different", + Name: "Preserve existing data restore host backup data source pvc with stale fake volume name after populated pv exists", 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", - }, - }, + dataProtectionBackupPendingPvcWithVolumeName.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), }, + InitialPhysicalState: []runtime.Object{dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.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"): {dataProtectionBackupPendingPvcWithVolumeNameBoundStatus.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"): {dataProtectionNoDataHostPendingWithBackupSourceAndFakeVolumeName.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( + 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 - pPVC := &corev1.PersistentVolumeClaim{} - err = syncCtx.HostClient.Get(syncCtx, types.NamespacedName{ - Namespace: pObjectMeta.Namespace, - Name: pObjectMeta.Name, - }, pPVC) + 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 - _, err = syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEvent(pPVC.DeepCopy(), vPVC.DeepCopy())) + 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()}, + }, + 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 + + pObjOld := backwardUpdateStatusPvc.DeepCopy() + pObj := backwardUpdateStatusPvc.DeepCopy() + vObjOld := backwardUpdateVolumeNameOnlyPvc.DeepCopy() + vObj := backwardUpdateVolumeNameOnlyPvc.DeepCopy() + + _, err := syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEventWithOld( + pObjOld, + pObj, + vObjOld, + vObj, + )) + assert.NilError(t, err) + }, + }, + { + Name: "Preserve data protection populated virtual status while host pvc waits for volume", + InitialVirtualState: []runtime.Object{ + dataProtectionBackupPvc.DeepCopy(), + dataProtectionPopulatedPV.DeepCopy(), + }, + InitialPhysicalState: []runtime.Object{dataProtectionHostPendingPvc.DeepCopy()}, + ExpectedVirtualState: map[schema.GroupVersionKind][]runtime.Object{ + corev1.SchemeGroupVersion.WithKind("PersistentVolumeClaim"): {dataProtectionBackupPvc.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(), + dataProtectionBackupPvc.DeepCopy(), + dataProtectionBackupPvc.DeepCopy(), + )) + 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{ + 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, + }, pPVC) + assert.NilError(t, err) + + _, err = syncer.(*persistentVolumeClaimSyncer).Sync(syncCtx, synccontext.NewSyncEvent(pPVC.DeepCopy(), vPVC.DeepCopy())) + assert.NilError(t, err) + }, + }, + }) +} + +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 }