From beb3a85e69046d8393197eb4672300afd5228a4d Mon Sep 17 00:00:00 2001 From: CSM Bot <105446864+csmbot@users.noreply.github.com> Date: Tue, 12 May 2026 15:24:00 -0400 Subject: [PATCH] Mirror internal repository with cleaned references --- Dockerfile | 2 +- README.md | 1 - cmd/csi-migrator/main_test.go | 12 + cmd/csi-node-rescanner/main_test.go | 12 + cmd/csi-replicator/main_test.go | 12 + cmd/replication-controller/main.go | 32 +- cmd/replication-controller/main_test.go | 14 +- config/manager/manager.yaml | 2 +- config/rbac/role.yaml | 8 + controllers/common.go | 3 + controllers/constants.go | 2 + controllers/csi-replicator/monitoring.go | 2 +- .../dellcsireplicationgroup_controller.go | 418 ++++- ...dellcsireplicationgroup_controller_test.go | 1374 +++++++++++++++++ .../persistentvolume_controller.go | 16 +- .../persistentvolumeclaim_controller.go | 24 +- .../persistentvolumeclaim_controller_test.go | 85 +- core/semver/semver_test.go | 2 +- deploy/controller.yaml | 6 +- deploy/role.yaml | 8 + go.mod | 45 +- go.sum | 141 +- helper.mk | 2 +- images.mk | 6 +- pkg/config/config_test.go | 2 +- pkg/connection/interface.go | 3 + pkg/connection/k8sconnections.go | 15 + repctl/cmd/repctl/main.go | 2 +- repctl/cmd/repctl/main_test.go | 1 - repctl/go.mod | 35 +- repctl/go.sum | 88 +- repctl/pkg/cmd/edit.go | 4 +- scripts/install.sh | 5 +- test/e2e-framework/fake-client/fake-client.go | 5 + test/mock-server/server/server.go | 7 +- 35 files changed, 2123 insertions(+), 273 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9e2bc4f4..44e1a51b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ FROM $BASEIMAGE AS container-base ARG VERSION LABEL vendor="Dell Technologies" \ maintainer="Dell Technologies" \ - release="1.16.0" \ + release="1.17.0" \ license="Apache-2.0" FROM container-base AS controller diff --git a/README.md b/README.md index 9bc090b1..037da4cc 100644 --- a/README.md +++ b/README.md @@ -147,4 +147,3 @@ If you wish to install CSM Replication Controller with repctl on your openSUSE s Click [here](/TESTING.md) for details on how to test. - diff --git a/cmd/csi-migrator/main_test.go b/cmd/csi-migrator/main_test.go index b2ed9486..e8b5e356 100644 --- a/cmd/csi-migrator/main_test.go +++ b/cmd/csi-migrator/main_test.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" @@ -45,6 +46,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) type mockManager struct { @@ -176,6 +178,16 @@ func (m *mockManager) GetScheme() *runtime.Scheme { return nil } +func (m *mockManager) GetConverterRegistry() conversion.Registry { + // Implement the method as needed for your mock + return nil +} + +func (m *mockManager) GetEventRecorder(_ string) events.EventRecorder { + // Implement the method as needed for your mock + return nil +} + func TestProcessConfigMapChanges(t *testing.T) { defaultGetUpdateConfigMapFunc := getUpdateConfigMapFunc defer func() { diff --git a/cmd/csi-node-rescanner/main_test.go b/cmd/csi-node-rescanner/main_test.go index 6cf30e44..65170415 100644 --- a/cmd/csi-node-rescanner/main_test.go +++ b/cmd/csi-node-rescanner/main_test.go @@ -41,6 +41,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" @@ -52,6 +53,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) func TestGetCSIConn(t *testing.T) { @@ -915,6 +917,16 @@ func (m *MockManager) GetWebhookServer() webhook.Server { return nil } +func (m *MockManager) GetConverterRegistry() conversion.Registry { + // Implement the GetConverterRegistry method logic + return nil +} + +func (m *MockManager) GetEventRecorder(_ string) events.EventRecorder { + // Implement the GetEventRecorder method logic + return nil +} + // MockEventRecorder is a mock implementation of the record.EventRecorder interface type MockEventRecorder struct{} diff --git a/cmd/csi-replicator/main_test.go b/cmd/csi-replicator/main_test.go index 70a19b98..1b9868a9 100644 --- a/cmd/csi-replicator/main_test.go +++ b/cmd/csi-replicator/main_test.go @@ -40,6 +40,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" @@ -51,6 +52,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) type mockManager struct { @@ -174,6 +176,16 @@ func (m *mockManager) GetScheme() *runtime.Scheme { return nil } +func (m *mockManager) GetConverterRegistry() conversion.Registry { + // Implement the method as needed for your mock + return nil +} + +func (m *mockManager) GetEventRecorder(_ string) events.EventRecorder { + // Implement the method as needed for your mock + return nil +} + func TestCreateReplicatorManager(t *testing.T) { // Original function references originalGetControllerManagerOpts := getControllerManagerOpts diff --git a/cmd/replication-controller/main.go b/cmd/replication-controller/main.go index 46518986..73020a79 100644 --- a/cmd/replication-controller/main.go +++ b/cmd/replication-controller/main.go @@ -104,11 +104,12 @@ var ( setupFlags = func() (map[string]string, logr.Logger, *logrus.Logger, context.Context) { var ( - retryIntervalStart time.Duration - retryIntervalMax time.Duration - workerThreads int - domain string - disablePVCRemap bool + retryIntervalStart time.Duration + retryIntervalMax time.Duration + workerThreads int + domain string + disablePVCRemap bool + enableKubevirtPVCRemap bool ) var metricsAddr string @@ -124,6 +125,7 @@ var ( flag.DurationVar(&retryIntervalMax, "retry-interval-max", 5*time.Minute, "Maximum retry interval of failed reconcile request") flag.IntVar(&workerThreads, "worker-threads", 2, "Number of concurrent reconcilers for each of the controllers") flag.BoolVar(&disablePVCRemap, "disable-pvc-remap", false, "disables PVC remapping functionality") + flag.BoolVar(&enableKubevirtPVCRemap, "enable-kubevirt-pvc-remap", false, "enables KubeVirt PVC remapping functionality") flag.BoolVar(&allowPVCCreationOnTarget, "allow-pvc-creation-on-target", false, "allow PVC creation on target cluster") flag.Parse() @@ -147,6 +149,7 @@ var ( flagMap["retry-interval-max"] = retryIntervalMax.String() flagMap["worker-threads"] = strconv.Itoa(workerThreads) flagMap["disable-pvc-remap"] = strconv.FormatBool(disablePVCRemap) + flagMap["enable-kubevirt-pvc-remap"] = strconv.FormatBool(enableKubevirtPVCRemap) flagMap["allow-pvc-creation-on-target"] = strconv.FormatBool(allowPVCCreationOnTarget) return flagMap, setupLog, logrusLog, context.Background() @@ -305,7 +308,7 @@ func main() { createPersistentVolumeClaimReconciler(mgr, controllerMgr, flagMap["prefix"], stringToInt(flagMap["worker-threads"]), expRateLimiter, stringToBoolean(flagMap["allow-pvc-creation-on-target"]), setupLog) // Create ReplicationGroupReconciler - createReplicationGroupReconciler(mgr, controllerMgr, flagMap["prefix"], stringToInt(flagMap["worker-threads"]), expRateLimiter, stringToBoolean(flagMap["disable-pvc-remap"]), setupLog) + createReplicationGroupReconciler(mgr, controllerMgr, flagMap["prefix"], stringToInt(flagMap["worker-threads"]), expRateLimiter, stringToBoolean(flagMap["disable-pvc-remap"]), stringToBoolean(flagMap["enable-kubevirt-pvc-remap"]), setupLog) // Create PersistentVolumeReconciler createPersistentVolumeReconciler(mgr, controllerMgr, flagMap["prefix"], stringToInt(flagMap["worker-threads"]), expRateLimiter, setupLog) @@ -345,15 +348,16 @@ func createPersistentVolumeReconciler(mgr manager.Manager, controllerMgr *Contro } } -func createReplicationGroupReconciler(mgr manager.Manager, controllerMgr *ControllerManager, domain string, workerThreads int, expRateLimiter workqueue.TypedRateLimiter[reconcile.Request], disablePVCRemap bool, setupLog logr.Logger) { +func createReplicationGroupReconciler(mgr manager.Manager, controllerMgr *ControllerManager, domain string, workerThreads int, expRateLimiter workqueue.TypedRateLimiter[reconcile.Request], disablePVCRemap bool, enableKubevirtPVCRemap bool, setupLog logr.Logger) { if err := getReplicationGroupReconciler(&repController.ReplicationGroupReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("DellCSIReplicationGroup"), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor(constants.DellReplicationController), - Config: controllerMgr.config, - Domain: domain, - DisablePVCRemap: disablePVCRemap, + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("DellCSIReplicationGroup"), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor(constants.DellReplicationController), + Config: controllerMgr.config, + Domain: domain, + DisablePVCRemap: disablePVCRemap, + EnableKubevirtPVCRemap: enableKubevirtPVCRemap, }, mgr, expRateLimiter, workerThreads); err != nil { setupLog.Error(err, "unable to create controller", constants.DellReplicationController, "DellCSIReplicationGroup") osExit(1) diff --git a/cmd/replication-controller/main_test.go b/cmd/replication-controller/main_test.go index 2c3a036b..c4b3bac3 100644 --- a/cmd/replication-controller/main_test.go +++ b/cmd/replication-controller/main_test.go @@ -36,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/events" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" @@ -47,6 +48,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" ) type mockManager struct { @@ -178,6 +180,16 @@ func (m *mockManager) GetScheme() *runtime.Scheme { return nil } +func (m *mockManager) GetConverterRegistry() conversion.Registry { + // Implement the method as needed for your mock + return nil +} + +func (m *mockManager) GetEventRecorder(_ string) events.EventRecorder { + // Implement the method as needed for your mock + return nil +} + type mockSecretController struct { mock.Mock logger logr.Logger @@ -943,7 +955,7 @@ func TestCreateReplicationGroupReconciler(t *testing.T) { } }() } - createReplicationGroupReconciler(tt.manager, tt.controllerMgr, tt.domain, tt.workerThreads, tt.expRateLimiter, false, tt.setupLog) + createReplicationGroupReconciler(tt.manager, tt.controllerMgr, tt.domain, tt.workerThreads, tt.expRateLimiter, false, false, tt.setupLog) if tt.name == "Manager is not nil" { if exitCode != 0 { t.Errorf("Expected exit code 0, but got %d", exitCode) diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 01dba275..57eefbd4 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -34,7 +34,7 @@ spec: args: - --enable-leader-election - --prefix=replication.storage.dell.com - image: quay.io/dell/container-storage-modules/dell-replication-controller:v1.13.0 + image: quay.io/dell/container-storage-modules/dell-replication-controller:v1.15.0 imagePullPolicy: IfNotPresent name: manager env: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3e625e38..bb5d928f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -127,6 +127,14 @@ rules: - get - list - watch + - apiGroups: + - cdi.kubevirt.io + resources: + - datavolumes + verbs: + - get + - update + - delete --- # Role for Driver-specific Permissions in a Namespace kind: Role diff --git a/controllers/common.go b/controllers/common.go index c9b033c6..8d4ccebd 100644 --- a/controllers/common.go +++ b/controllers/common.go @@ -244,6 +244,8 @@ var ( MigrationGroup string // MigrationFinalizer — finalizer used by the migration sidecar for pre delete hook MigrationFinalizer string + // PendingPVCSwap annotation on the local PV storing serialized PVC backup for crash recovery during PVC swap + PendingPVCSwap string ) // InitLabelsAndAnnotations initializes package visible constants by using customizable domain variable @@ -283,4 +285,5 @@ func InitLabelsAndAnnotations(domain string) { ActionProcessedTime = domain + actionProcessedTime MigrationGroup = domain + migrationGroup MigrationFinalizer = domain + migrationFinalizer + PendingPVCSwap = domain + pendingPVCSwap } diff --git a/controllers/constants.go b/controllers/constants.go index 928158b0..70d039b3 100644 --- a/controllers/constants.go +++ b/controllers/constants.go @@ -133,6 +133,8 @@ const ( ClusterUID = "clusterUID" // NodeReScanned will flag the current rescan status NodeReScanned = "node-rescanned" + // pendingPVCSwap annotation on the local PV storing serialized PVC backup for crash recovery during PVC swap + pendingPVCSwap = "/pendingPVCSwap" XCSIReplicationPodName = "X_CSI_REPLICATION_POD_NAME" XCSIReplicationPodNamespace = "X_CSI_REPLICATION_POD_NAMESPACE" diff --git a/controllers/csi-replicator/monitoring.go b/controllers/csi-replicator/monitoring.go index 884e509e..e38ff57a 100644 --- a/controllers/csi-replicator/monitoring.go +++ b/controllers/csi-replicator/monitoring.go @@ -15,6 +15,7 @@ package csireplicator import ( + "context" "fmt" "sync" "time" @@ -25,7 +26,6 @@ import ( csireplication "github.com/dell/csm-replication/pkg/csi-clients/replication" "github.com/dell/dell-csi-extensions/replication" "github.com/go-logr/logr" - "golang.org/x/net/context" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" diff --git a/controllers/replication-controller/dellcsireplicationgroup_controller.go b/controllers/replication-controller/dellcsireplicationgroup_controller.go index 8c903a85..538e6402 100644 --- a/controllers/replication-controller/dellcsireplicationgroup_controller.go +++ b/controllers/replication-controller/dellcsireplicationgroup_controller.go @@ -37,7 +37,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" @@ -91,18 +93,28 @@ var ( sleep = func(d time.Duration) { time.Sleep(d) } + getObject = func(ctx context.Context, c connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + return c.GetObject(ctx, key, obj) + } + updateObject = func(ctx context.Context, c connection.RemoteClusterClient, obj client.Object) error { + return c.UpdateObject(ctx, obj) + } + deleteObject = func(ctx context.Context, c connection.RemoteClusterClient, obj client.Object) error { + return c.DeleteObject(ctx, obj) + } ) // ReplicationGroupReconciler reconciles a ReplicationGroup object type ReplicationGroupReconciler struct { client.Client - Log logr.Logger - Scheme *runtime.Scheme - EventRecorder record.EventRecorder - PVCRequeueInterval time.Duration - Config connection.MultiClusterClient - Domain string - DisablePVCRemap bool + Log logr.Logger + Scheme *runtime.Scheme + EventRecorder record.EventRecorder + PVCRequeueInterval time.Duration + Config connection.MultiClusterClient + Domain string + DisablePVCRemap bool + EnableKubevirtPVCRemap bool } // +kubebuilder:rbac:groups=replication.storage.dell.com,resources=dellcsireplicationgroups,verbs=get;list;watch;update;patch;delete;create @@ -586,18 +598,78 @@ func (r *ReplicationGroupReconciler) SetupWithManager(mgr ctrl.Manager, limiter Complete(r) } +// pvcSwapBackup stores PVC details and original PV reclaim policies for crash recovery during PVC swap. +// It is serialized as JSON and stored as an annotation on the local PV before any destructive action. +type pvcSwapBackup struct { + PVC *v1.PersistentVolumeClaim `json:"pvc"` + LocalPVPolicy v1.PersistentVolumeReclaimPolicy `json:"localPVPolicy"` + RemotePVPolicy v1.PersistentVolumeReclaimPolicy `json:"remotePVPolicy"` +} + +// savePVCBackupToPV persists the PVC swap backup as a JSON annotation on the local PV. +// It also sets the PV reclaim policy to Retain in the same update to minimize API calls. +func savePVCBackupToPV(ctx context.Context, c connection.RemoteClusterClient, localPV *v1.PersistentVolume, backup *pvcSwapBackup, log logr.Logger) error { + data, err := json.Marshal(backup) + if err != nil { + return fmt.Errorf("error marshaling PVC backup: %w", err) + } + if localPV.Annotations == nil { + localPV.Annotations = make(map[string]string) + } + localPV.Annotations[controller.PendingPVCSwap] = string(data) + localPV.Spec.PersistentVolumeReclaimPolicy = v1.PersistentVolumeReclaimRetain + if err := updatePersistentVolume(ctx, c, localPV); err != nil { + return fmt.Errorf("error saving PVC backup to PV %s: %w", localPV.Name, err) + } + log.V(logger.InfoLevel).Info(fmt.Sprintf("Saved PVC swap backup to PV %s and set Retain policy", localPV.Name)) + return nil +} + +// recoverPVCBackup attempts to recover a PVC swap backup from the local PV. +// It uses the target PV's RemotePV annotation to find the local PV name. +func recoverPVCBackup(ctx context.Context, c connection.RemoteClusterClient, targetPV string, log logr.Logger) (*pvcSwapBackup, error) { + remotePV, err := getPersistentVolume(ctx, c, targetPV) + if err != nil { + return nil, fmt.Errorf("error getting target PV %s for recovery: %w", targetPV, err) + } + localPVName := remotePV.Annotations[controller.RemotePV] + if localPVName == "" { + return nil, fmt.Errorf("target PV %s has no RemotePV annotation", targetPV) + } + localPV, err := getPersistentVolume(ctx, c, localPVName) + if err != nil { + return nil, fmt.Errorf("error getting local PV %s for recovery: %w", localPVName, err) + } + backupJSON, ok := localPV.Annotations[controller.PendingPVCSwap] + if !ok { + return nil, fmt.Errorf("local PV %s has no pending PVC swap annotation", localPVName) + } + var backup pvcSwapBackup + if err := json.Unmarshal([]byte(backupJSON), &backup); err != nil { + return nil, fmt.Errorf("error unmarshaling PVC backup from PV %s: %w", localPVName, err) + } + log.V(logger.InfoLevel).Info(fmt.Sprintf("Recovered PVC swap backup from PV %s", localPVName)) + return &backup, nil +} + // Give a replication group name and target, swapAllPVC reassigns the PVC from local volume to remote volume. // It also retains the original reclaimPolicy and operates within a single cluster. +// After processing listed PVCs, it checks for orphaned PVs with pending swap annotations +// to recover PVCs that were mid-swap when the controller crashed. func (r *ReplicationGroupReconciler) swapAllPVC(ctx context.Context, c connection.RemoteClusterClient, rgName string, rgTarget string, log logr.Logger) error { pvcs, err := c.ListPersistentVolumeClaim(ctx, client.MatchingLabels{controller.ReplicationGroup: rgName}) if err != nil { return fmt.Errorf("failed to list PVCs: %w", err) } + // Build set of PVC names being swapped (to detect orphans later) + swappedPVCNames := make(map[string]bool) + var wg sync.WaitGroup errChan := make(chan error, len(pvcs.Items)) for _, pvc := range pvcs.Items { + swappedPVCNames[pvc.Namespace+"/"+pvc.Name] = true wg.Add(1) go func(pvc v1.PersistentVolumeClaim) { defer wg.Done() @@ -618,6 +690,35 @@ func (r *ReplicationGroupReconciler) swapAllPVC(ctx context.Context, c connectio errs = append(errs, err) } + // Recovery: check for PVs with pending swap annotations whose PVCs were not in the list. + // This handles the case where the controller crashed after PVC deletion but before recreation. + var pvList v1.PersistentVolumeList + if listErr := r.Client.List(ctx, &pvList, client.MatchingLabels{controller.ReplicationGroup: rgName}); listErr == nil { + for i := range pvList.Items { + pv := &pvList.Items[i] + backupJSON, ok := pv.Annotations[controller.PendingPVCSwap] + if !ok { + continue + } + var backup pvcSwapBackup + if err := json.Unmarshal([]byte(backupJSON), &backup); err != nil { + log.V(logger.WarnLevel).Info(fmt.Sprintf("Failed to unmarshal PVC backup from PV %s: %v", pv.Name, err)) + continue + } + key := backup.PVC.Namespace + "/" + backup.PVC.Name + if swappedPVCNames[key] { + continue // already handled in the main loop + } + targetPV := backup.PVC.Annotations[controller.RemotePV] + log.V(logger.InfoLevel).Info(fmt.Sprintf("Recovering orphaned PVC swap for %s from PV %s", key, pv.Name)) + if err := r.swapPVC(ctx, c, backup.PVC.Name, backup.PVC.Namespace, targetPV, rgTarget, log); err != nil { + errs = append(errs, fmt.Errorf("error recovering PVC swap %s: %s", key, err)) + } + } + } else { + log.V(logger.WarnLevel).Info(fmt.Sprintf("Failed to list PVs for swap recovery: %v", listErr)) + } + if len(errs) > 0 { return fmt.Errorf("errors occurred while swapping PVCs: %s", errs) } @@ -626,76 +727,141 @@ func (r *ReplicationGroupReconciler) swapAllPVC(ctx context.Context, c connectio } func (r *ReplicationGroupReconciler) swapPVC(ctx context.Context, client connection.RemoteClusterClient, pvcName, namespace, targetPV, rgTarget string, log logr.Logger) error { + var pvc *v1.PersistentVolumeClaim + var localPVPolicy, remotePVPolicy v1.PersistentVolumeReclaimPolicy + recovered := false + // Read the PVC - pvc, err := getPersistentVolumeClaim(ctx, client, namespace, pvcName) + pvcFetched, err := getPersistentVolumeClaim(ctx, client, namespace, pvcName) if err != nil { - return fmt.Errorf("error getting pvc %s: %s", pvcName, err) + if !errors.IsNotFound(err) { + return fmt.Errorf("error getting pvc %s: %s", pvcName, err) + } + // PVC not found — attempt recovery from local PV backup annotation + backup, recoveryErr := recoverPVCBackup(ctx, client, targetPV, log) + if recoveryErr != nil { + return fmt.Errorf("PVC %s/%s not found and recovery failed: %w", namespace, pvcName, recoveryErr) + } + pvc = backup.PVC + localPVPolicy = backup.LocalPVPolicy + remotePVPolicy = backup.RemotePVPolicy + recovered = true + log.V(logger.InfoLevel).Info(fmt.Sprintf("Recovered PVC %s/%s from PV backup, skipping to recreation", namespace, pvcName)) + } else { + pvc = pvcFetched + + // If KubeVirt PVC remap is disabled but the PVC is owned by a DataVolume, + // skip the swap entirely — we cannot safely delete a DV-owned PVC without + // first handling the DataVolume dependency. + if !r.EnableKubevirtPVCRemap && findDataVolumeOwnerRef(pvc.OwnerReferences) != nil { + log.V(logger.WarnLevel).Info(fmt.Sprintf("PVC %s/%s is owned by a DataVolume but KubeVirt PVC remap is disabled; skipping PVC swap", + pvc.Namespace, pvc.Name)) + return nil + } } - // Save the Reclaim Policy for both PVs - return reclaim policy to makepvcreclaimpolicyretain - pv, err := getPersistentVolume(ctx, client, pvc.Spec.VolumeName) - if err != nil { - return fmt.Errorf("error retrieving local PV %s", pvc.Spec.VolumeName) - } - localPVPolicy := pv.Spec.PersistentVolumeReclaimPolicy + if !recovered { + // Normal flow: read PVs, save backup, set Retain, handle DV, delete/wait - pv, err = getPersistentVolume(ctx, client, pvc.Annotations[controller.RemotePV]) - if err != nil { - return fmt.Errorf("error retrieving remote PV %s", pvc.Annotations[controller.RemotePV]) - } + // Save the Reclaim Policy for both PVs + localPV, err := getPersistentVolume(ctx, client, pvc.Spec.VolumeName) + if err != nil { + return fmt.Errorf("error retrieving local PV %s", pvc.Spec.VolumeName) + } + localPVPolicy = localPV.Spec.PersistentVolumeReclaimPolicy - // The target PV should be unclaimed. - if pv.Spec.ClaimRef != nil { - // Check if the target PV claimRef if set to "reserved/reserved" This is done as part of claimRef feature - if pv.Spec.ClaimRef.Name == controller.ReservedPVCName && pv.Spec.ClaimRef.Namespace == controller.ReservedPVCNamespace { - // Update the claimRef to nil so that PVC can be created - err = removeReservedClaimRefForTargetPV(ctx, client, pv.Name, log) - if err != nil { - return fmt.Errorf("error removing PV claim ref from %s: %s", pv, err.Error()) + remotePV, err := getPersistentVolume(ctx, client, pvc.Annotations[controller.RemotePV]) + if err != nil { + return fmt.Errorf("error retrieving remote PV %s", pvc.Annotations[controller.RemotePV]) + } + + // The target PV should be unclaimed. + if remotePV.Spec.ClaimRef != nil { + if remotePV.Spec.ClaimRef.Name == controller.ReservedPVCName && remotePV.Spec.ClaimRef.Namespace == controller.ReservedPVCNamespace { + err = removeReservedClaimRefForTargetPV(ctx, client, remotePV.Name, log) + if err != nil { + return fmt.Errorf("error removing PV claim ref from %s: %s", remotePV, err.Error()) + } + } else { + // During recovery, the PVC might be deleted but the PV still has the ClaimRef. + // Check if the PVC still exists before failing. + _, pvcErr := getPersistentVolumeClaim(ctx, client, remotePV.Spec.ClaimRef.Namespace, remotePV.Spec.ClaimRef.Name) + if pvcErr != nil && errors.IsNotFound(pvcErr) { + // PVC doesn't exist, safe to remove the ClaimRef + log.V(logger.InfoLevel).Info(fmt.Sprintf("PVC %s/%s not found, removing stale ClaimRef from target PV %s", + remotePV.Spec.ClaimRef.Namespace, remotePV.Spec.ClaimRef.Name, remotePV.Name)) + remotePV.Spec.ClaimRef = nil + if err := updatePersistentVolume(ctx, client, remotePV); err != nil { + return fmt.Errorf("error removing stale ClaimRef from target PV %s: %s", remotePV.Name, err) + } + } else { + return fmt.Errorf("target PV %s is claimed by PVC %s/%s", remotePV.Name, + remotePV.Spec.ClaimRef.Namespace, remotePV.Spec.ClaimRef.Name) + } } - } else { - return fmt.Errorf("target PV %s is claimed", pv.Name) } - } - remotePVPolicy := pv.Spec.PersistentVolumeReclaimPolicy + remotePVPolicy = remotePV.Spec.PersistentVolumeReclaimPolicy - // Make the local PV reclaim policy Retain - if err = setPVReclaimPolicy(ctx, client, pvc.Spec.VolumeName, "Retain"); err != nil { - return fmt.Errorf("error making source PV %s reclaim policy Retain: %s", pvc.Spec.VolumeName, err) - } - - // Make the remote PV reclaim policy Retain - if err = setPVReclaimPolicy(ctx, client, pvc.Annotations[controller.RemotePV], "Retain"); err != nil { - return fmt.Errorf("error making target PV %s reclaim policy Retain: %s", pvc.Annotations[controller.RemotePV], err) - } + // Save PVC backup to local PV and set Retain in a single update. + // This persists the PVC details so they survive a pod crash. + backup := &pvcSwapBackup{ + PVC: pvc, + LocalPVPolicy: localPVPolicy, + RemotePVPolicy: remotePVPolicy, + } + if err := savePVCBackupToPV(ctx, client, localPV, backup, log); err != nil { + return err + } - // Delete the existing PVC - err = deletePersistentVolumeClaim(ctx, client, pvc) - if err != nil { - return fmt.Errorf("error deleting PVC %s with errror %s", pvcName, err) - } + // Make the remote PV reclaim policy Retain + if err = setPVReclaimPolicy(ctx, client, pvc.Annotations[controller.RemotePV], "Retain"); err != nil { + return fmt.Errorf("error making target PV %s reclaim policy Retain: %s", pvc.Annotations[controller.RemotePV], err) + } - // Wait until PVC is deleted - done := false - for iteration := 0; !done; iteration++ { - sleep(2 * time.Second) - _, err := getPersistentVolumeClaim(ctx, client, namespace, pvcName) - if err != nil { - if errors.IsNotFound(err) { - done = true - break + // Handle DataVolume dependencies before PVC deletion. + // If the DV was deleted, Kubernetes GC will cascade-delete the PVC. + // If no DV was involved, we delete the PVC explicitly below. + dvDeleted := false + if r.EnableKubevirtPVCRemap { + var dvErr error + dvDeleted, dvErr = r.handleDataVolumeDependencies(ctx, client, pvc, log) + if dvErr != nil { + return dvErr } + } - return fmt.Errorf("error when waiting for PVC %s/%s to be deleted", namespace, pvcName) + if !dvDeleted { + // No DV involvement — delete the PVC explicitly + err = deletePersistentVolumeClaim(ctx, client, pvc) + if err != nil { + return fmt.Errorf("error deleting PVC %s with errror %s", pvcName, err) + } } - if iteration > 30 { - return fmt.Errorf("timed out waiting on PVC %s/%s to be deleted", namespace, pvcName) + // Wait until PVC is deleted (either by GC cascade or explicit delete above) + done := false + for iteration := 0; !done; iteration++ { + sleep(2 * time.Second) + _, err := getPersistentVolumeClaim(ctx, client, namespace, pvcName) + if err != nil { + if errors.IsNotFound(err) { + done = true + break + } + + return fmt.Errorf("error when waiting for PVC %s/%s to be deleted", namespace, pvcName) + } + + if iteration > 30 { + return fmt.Errorf("timed out waiting on PVC %s/%s to be deleted", namespace, pvcName) + } } } - // Swap some fields in the PVC. + // --- Common recreation path (normal + recovery) --- + + // Swap some fields in the PVC (using the in-memory or recovered copy). localPV := pvc.Spec.VolumeName pvc.Annotations[controller.RemotePV] = pvc.Spec.VolumeName pvc.Spec.VolumeName = targetPV @@ -707,6 +873,18 @@ func (r *ReplicationGroupReconciler) swapPVC(ctx context.Context, client connect pvc.Spec.StorageClassName = &remoteStorageClassName pvc.ObjectMeta.ResourceVersion = "" + // Strip DataVolume/CDI artifacts from the PVC before recreation so the new PVC + // is not tied to the deleted DataVolume or CDI import sources. + if r.EnableKubevirtPVCRemap { + pvc.OwnerReferences = removeDataVolumeOwnerRef(pvc.OwnerReferences) + removeCDIAnnotations(pvc) + } + pvc.Spec.DataSource = nil + pvc.Spec.DataSourceRef = nil + + // Remove the pending swap annotation before recreation + delete(pvc.Annotations, controller.PendingPVCSwap) + // Re-create the PVC, now pointing to the target. err = createPersistentVolumeClaim(ctx, client, pvc) if err != nil { @@ -734,8 +912,8 @@ func (r *ReplicationGroupReconciler) swapPVC(ctx context.Context, client connect return fmt.Errorf("error removing PV claim ref from %s: %s", localPV, err.Error()) } - // Updating the claimRef of localPV to reservd/reserved - pv, err = getPersistentVolume(ctx, client, localPV) + // Updating the claimRef of localPV to reserved/reserved and clear backup annotation + pv, err := getPersistentVolume(ctx, client, localPV) if err != nil { return fmt.Errorf("error retrieving PV %s: %s", localPV, err.Error()) } @@ -747,6 +925,7 @@ func (r *ReplicationGroupReconciler) swapPVC(ctx context.Context, client connect Namespace: controller.ReservedPVCNamespace, } pv.Spec.ClaimRef = claimRef + delete(pv.Annotations, controller.PendingPVCSwap) err = updatePersistentVolume(ctx, client, pv) if err != nil { @@ -914,3 +1093,122 @@ func updatePVClaimRef(ctx context.Context, client connection.RemoteClusterClient return fmt.Errorf("timed out updating the claim ref") } + +// handleDataVolumeDependencies detects and handles DataVolume ownership on the PVC +// before PVC deletion during remap. Returns dvDeleted=true if a DataVolume was deleted, +// meaning the PVC will be garbage-collected by Kubernetes and should not be deleted explicitly. +// +// 1. If the PVC has no DataVolume ownerRef: return (false, nil). +// 2. If the PVC has a DataVolume ownerRef, fetch the DV. +// a. If the DV is not found: return (false, nil) — DV already gone, PVC may still exist. +// b. If the DV is owned by a VirtualMachine: return (false, error) — skip cleanup. +// c. If the DV is NOT owned by a VM: remove finalizers, delete DV, return (true, nil). +// +// When dvDeleted=true, the caller should NOT explicitly delete the PVC. Kubernetes garbage +// collection will cascade-delete the PVC via the DV ownerReference. The caller should wait +// for PVC deletion before recreating it. +func (r *ReplicationGroupReconciler) handleDataVolumeDependencies(ctx context.Context, c connection.RemoteClusterClient, pvc *v1.PersistentVolumeClaim, log logr.Logger) (bool, error) { + dvOwnerRef := findDataVolumeOwnerRef(pvc.OwnerReferences) + if dvOwnerRef == nil { + return false, nil + } + + log.V(logger.InfoLevel).Info(fmt.Sprintf("PVC %s/%s has DataVolume ownerRef %s, fetching DV to check ownership", + pvc.Namespace, pvc.Name, dvOwnerRef.Name)) + + // Fetch the DataVolume to inspect its ownerReferences + dv := &unstructured.Unstructured{} + dv.SetGroupVersionKind(schema.FromAPIVersionAndKind(dvOwnerRef.APIVersion, dvOwnerRef.Kind)) + err := getObject(ctx, c, client.ObjectKey{Name: dvOwnerRef.Name, Namespace: pvc.Namespace}, dv) + if err != nil { + if errors.IsNotFound(err) { + // DV already deleted — PVC may still exist, caller should delete it explicitly + log.V(logger.InfoLevel).Info(fmt.Sprintf("DataVolume %s/%s not found, PVC deletion will be handled by caller", + pvc.Namespace, dvOwnerRef.Name)) + return false, nil + } + return false, fmt.Errorf("error fetching DataVolume %s/%s: %w", pvc.Namespace, dvOwnerRef.Name, err) + } + + // Check if the DV is owned by a VirtualMachine + vmOwnerRef := findVMOwnerRef(dv.GetOwnerReferences()) + if vmOwnerRef != nil { + msg := fmt.Sprintf("PVC %s/%s is backed by DataVolume %s owned by VirtualMachine %s; skipping cleanup — "+ + "stop the VM and remove the dataVolumeTemplate before retrying PVC remap", + pvc.Namespace, pvc.Name, dvOwnerRef.Name, vmOwnerRef.Name) + log.V(logger.WarnLevel).Info(msg) + r.EventRecorder.Event(pvc, eventTypeWarning, "VirtualMachineOwnedDataVolume", msg) + return false, fmt.Errorf("%s", msg) + } + + // DV is NOT owned by a VM — remove finalizers and delete DV. + // Kubernetes GC will cascade-delete the PVC via the ownerReference. + log.V(logger.InfoLevel).Info(fmt.Sprintf("DataVolume %s/%s is standalone (not VM-owned), removing finalizers and deleting", + pvc.Namespace, dvOwnerRef.Name)) + + // Remove finalizers from DV so deletion is immediate + if len(dv.GetFinalizers()) > 0 { + log.V(logger.InfoLevel).Info(fmt.Sprintf("Removing finalizers %v from DataVolume %s/%s", + dv.GetFinalizers(), pvc.Namespace, dvOwnerRef.Name)) + dv.SetFinalizers(nil) + if err := updateObject(ctx, c, dv); err != nil { + return false, fmt.Errorf("error removing finalizers from DataVolume %s/%s: %w", + pvc.Namespace, dvOwnerRef.Name, err) + } + } + + if err := deleteObject(ctx, c, dv); err != nil { + if !errors.IsNotFound(err) { + return false, fmt.Errorf("error deleting DataVolume %s/%s: %w", pvc.Namespace, dvOwnerRef.Name, err) + } + } + + log.V(logger.InfoLevel).Info(fmt.Sprintf("Deleted DataVolume %s/%s; PVC will be garbage-collected", + pvc.Namespace, dvOwnerRef.Name)) + return true, nil +} + +// findDataVolumeOwnerRef returns the first OwnerReference that points to a DataVolume (cdi.kubevirt.io). +func findDataVolumeOwnerRef(refs []metav1.OwnerReference) *metav1.OwnerReference { + for i, ref := range refs { + if strings.Contains(ref.APIVersion, "cdi.kubevirt.io") && ref.Kind == "DataVolume" { + return &refs[i] + } + } + return nil +} + +// findVMOwnerRef returns the first OwnerReference that points to a VirtualMachine (kubevirt.io). +func findVMOwnerRef(refs []metav1.OwnerReference) *metav1.OwnerReference { + for i, ref := range refs { + if strings.Contains(ref.APIVersion, "kubevirt.io") && ref.Kind == "VirtualMachine" { + return &refs[i] + } + } + return nil +} + +// removeDataVolumeOwnerRef returns the ownerReferences list with all DataVolume (cdi.kubevirt.io) entries removed. +func removeDataVolumeOwnerRef(refs []metav1.OwnerReference) []metav1.OwnerReference { + filtered := make([]metav1.OwnerReference, 0, len(refs)) + for _, ref := range refs { + if strings.Contains(ref.APIVersion, "cdi.kubevirt.io") && ref.Kind == "DataVolume" { + continue + } + filtered = append(filtered, ref) + } + return filtered +} + +// removeCDIAnnotations removes all CDI-related annotations from the PVC. +// CDI annotations use the "cdi.kubevirt.io" prefix. +func removeCDIAnnotations(pvc *v1.PersistentVolumeClaim) { + if pvc.Annotations == nil { + return + } + for key := range pvc.Annotations { + if strings.Contains(key, "cdi.kubevirt.io") { + delete(pvc.Annotations, key) + } + } +} diff --git a/controllers/replication-controller/dellcsireplicationgroup_controller_test.go b/controllers/replication-controller/dellcsireplicationgroup_controller_test.go index 900b7978..7bb86b58 100644 --- a/controllers/replication-controller/dellcsireplicationgroup_controller_test.go +++ b/controllers/replication-controller/dellcsireplicationgroup_controller_test.go @@ -36,7 +36,10 @@ import ( corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" @@ -1367,6 +1370,59 @@ func (suite *RGControllerTestSuite) TestPVCRemapDisabled() { suite.Nil(unchangedRemotePV.Spec.ClaimRef, "Remote PV's claim reference should remain nil") } +func (suite *RGControllerTestSuite) TestKubevirtPVCRemapDisabledByDefault() { + rg, _, _, _, pvcObj := suite.getSingleClusterPVSetup() + rgName := rg.Name + + // Ensure EnableKubevirtPVCRemap is false (default) + suite.reconciler.EnableKubevirtPVCRemap = false + + // Add a DataVolume ownerRef to simulate a kubevirt-backed PVC + trueVal := true + pvcObj.OwnerReferences = append(pvcObj.OwnerReferences, metav1.OwnerReference{ + APIVersion: "cdi.kubevirt.io/v1beta1", + Kind: "DataVolume", + Name: "test-dv", + UID: "dv-uid", + Controller: &trueVal, + BlockOwnerDeletion: &trueVal, + }) + err := suite.client.Update(context.Background(), pvcObj) + suite.NoError(err) + + // Invoke failover action + time := metav1.Now() + lastAction := repv1.LastAction{ + Time: &time, + Condition: "Action FAILOVER_REMOTE succeeded", + } + rg.Status = repv1.DellCSIReplicationGroupStatus{ + LastAction: lastAction, + Conditions: []repv1.LastAction{lastAction}, + } + rg.Annotations[controllers.ActionProcessedTime] = time.String() + + err = suite.client.Update(context.Background(), rg) + suite.NoError(err) + + // Reconcile to trigger processFailoverAction + req := suite.getTypicalRequest() + resp, err := suite.reconciler.Reconcile(context.Background(), req) + suite.NoError(err) + suite.Equal(false, resp.Requeue) + + // Verify that PVC swap was skipped — DV-owned PVCs should not be deleted + // when KubeVirt PVC remap is disabled + var updatedPVC corev1.PersistentVolumeClaim + err = suite.client.Get(context.Background(), types.NamespacedName{Name: "fake-pvc", Namespace: "fake-ns"}, &updatedPVC) + suite.NoError(err) + + // PVC should remain unchanged — still bound to local-pv + suite.Equal("local-pv", updatedPVC.Spec.VolumeName, "PVC should still be bound to local PV (swap skipped)") + suite.Equal("remote-pv", updatedPVC.Annotations[controllers.RemotePV], "Remote PV annotation should be unchanged") + suite.Equal(rgName, updatedPVC.Annotations[controllers.ReplicationGroup], "Replication group annotation should be unchanged") +} + func (suite *RGControllerTestSuite) TestPVCRemapWithMismatchedRemotePV() { rg, _, localPV, remotePV, pvcObj := suite.getSingleClusterPVSetup() rgName := rg.Name @@ -2083,3 +2139,1321 @@ func TestRemoveReservedClaimRefForTargetPV(t *testing.T) { }) } } + +func TestHandleDataVolumeDependencies(t *testing.T) { + originalGetObject := getObject + originalDeleteObject := deleteObject + originalUpdateObject := updateObject + + after := func() { + getObject = originalGetObject + deleteObject = originalDeleteObject + updateObject = originalUpdateObject + } + + trueVal := true + + tests := []struct { + name string + pvc *v1.PersistentVolumeClaim + setup func() + wantErr bool + wantErrMsg string + wantDVDeleted bool + validate func(t *testing.T, pvc *v1.PersistentVolumeClaim) + }{ + { + name: "No DataVolume ownerRef — no action", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "v1", Kind: "ConfigMap", Name: "cm"}, + }, + }, + }, + setup: func() {}, + wantErr: false, + wantDVDeleted: false, + validate: func(t *testing.T, pvc *v1.PersistentVolumeClaim) { + if len(pvc.OwnerReferences) != 1 { + t.Errorf("expected 1 ownerRef unchanged, got %d", len(pvc.OwnerReferences)) + } + }, + }, + { + name: "DV owned by VM — skip cleanup, return error", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid", Controller: &trueVal}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences([]metav1.OwnerReference{ + {APIVersion: "kubevirt.io/v1", Kind: "VirtualMachine", Name: "test-vm", UID: "vm-uid"}, + }) + return nil + } + }, + wantErr: true, + wantErrMsg: "skipping cleanup", + wantDVDeleted: false, + }, + { + name: "DV NOT owned by VM, no finalizers — delete DV, return dvDeleted=true", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid", Controller: &trueVal}, + {APIVersion: "v1", Kind: "ConfigMap", Name: "cm"}, + }, + Annotations: map[string]string{ + "cdi.kubevirt.io/storage.condition.running": "true", + "replication.storage.dell.com/remotePV": "remote-pv", + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences(nil) + return nil + } + deleteObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.Object) error { + return nil + } + }, + wantErr: false, + wantDVDeleted: true, + validate: func(t *testing.T, pvc *v1.PersistentVolumeClaim) { + // PVC should NOT be modified by handleDataVolumeDependencies + if len(pvc.OwnerReferences) != 2 { + t.Errorf("expected PVC ownerRefs unchanged (2), got %d", len(pvc.OwnerReferences)) + } + if _, ok := pvc.Annotations["cdi.kubevirt.io/storage.condition.running"]; !ok { + t.Error("expected PVC annotations unchanged") + } + }, + }, + { + name: "DV with finalizers — remove finalizers, delete DV, return dvDeleted=true", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid", Controller: &trueVal}, + }, + }, + }, + setup: func() { + finalizerRemoved := false + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences(nil) + u.SetFinalizers([]string{"cdi.kubevirt.io/dataVolumeFinalizer"}) + return nil + } + updateObject = func(_ context.Context, _ connection.RemoteClusterClient, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + if len(u.GetFinalizers()) != 0 { + t.Error("expected finalizers to be cleared before update") + } + finalizerRemoved = true + return nil + } + deleteObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.Object) error { + if !finalizerRemoved { + t.Error("expected finalizers to be removed before delete") + } + return nil + } + }, + wantErr: false, + wantDVDeleted: true, + }, + { + name: "DV not found — return dvDeleted=false (caller handles PVC deletion)", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid"}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.ObjectKey, _ client.Object) error { + return k8serrors.NewNotFound(schema.GroupResource{Group: "cdi.kubevirt.io", Resource: "datavolumes"}, "test-dv") + } + }, + wantErr: false, + wantDVDeleted: false, + validate: func(t *testing.T, pvc *v1.PersistentVolumeClaim) { + // PVC should NOT be modified + if len(pvc.OwnerReferences) != 1 { + t.Errorf("expected PVC ownerRefs unchanged, got %d", len(pvc.OwnerReferences)) + } + }, + }, + { + name: "Error fetching DV — return error", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid"}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.ObjectKey, _ client.Object) error { + return fmt.Errorf("connection refused") + } + }, + wantErr: true, + wantErrMsg: "error fetching DataVolume", + wantDVDeleted: false, + }, + { + name: "Error removing DV finalizers — return error", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid"}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences(nil) + u.SetFinalizers([]string{"cdi.kubevirt.io/dataVolumeFinalizer"}) + return nil + } + updateObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.Object) error { + return errors.New("update conflict") + } + }, + wantErr: true, + wantErrMsg: "error removing finalizers", + wantDVDeleted: false, + }, + { + name: "Error deleting DV — return error", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid"}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences(nil) + return nil + } + deleteObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.Object) error { + return errors.New("delete error") + } + }, + wantErr: true, + wantErrMsg: "error deleting DataVolume", + wantDVDeleted: false, + }, + { + name: "DV delete returns NotFound — idempotent, return dvDeleted=true", + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "test-dv", UID: "dv-uid"}, + }, + }, + }, + setup: func() { + getObject = func(_ context.Context, _ connection.RemoteClusterClient, key client.ObjectKey, obj client.Object) error { + u := obj.(*unstructured.Unstructured) + u.SetName(key.Name) + u.SetNamespace(key.Namespace) + u.SetOwnerReferences(nil) + return nil + } + deleteObject = func(_ context.Context, _ connection.RemoteClusterClient, _ client.Object) error { + return k8serrors.NewNotFound(schema.GroupResource{Group: "cdi.kubevirt.io", Resource: "datavolumes"}, "test-dv") + } + }, + wantErr: false, + wantDVDeleted: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + defer after() + + r := &ReplicationGroupReconciler{ + EventRecorder: record.NewFakeRecorder(10), + } + log := ctrl.Log.WithName("test") + + dvDeleted, err := r.handleDataVolumeDependencies(context.Background(), nil, tt.pvc, log) + if (err != nil) != tt.wantErr { + t.Errorf("handleDataVolumeDependencies() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErrMsg != "" && err != nil { + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Errorf("expected error containing %q, got %q", tt.wantErrMsg, err.Error()) + } + } + if dvDeleted != tt.wantDVDeleted { + t.Errorf("handleDataVolumeDependencies() dvDeleted = %v, want %v", dvDeleted, tt.wantDVDeleted) + } + if !tt.wantErr && tt.validate != nil { + tt.validate(t, tt.pvc) + } + }) + } +} + +func TestFindDataVolumeOwnerRef(t *testing.T) { + refs := []metav1.OwnerReference{ + {APIVersion: "v1", Kind: "ConfigMap", Name: "cm"}, + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "my-dv"}, + {APIVersion: "kubevirt.io/v1", Kind: "VirtualMachine", Name: "my-vm"}, + } + + result := findDataVolumeOwnerRef(refs) + if result == nil { + t.Fatal("expected to find DataVolume ownerRef") + } + if result.Name != "my-dv" { + t.Errorf("expected name my-dv, got %s", result.Name) + } + + // No DV ownerRef + noRefs := []metav1.OwnerReference{ + {APIVersion: "v1", Kind: "ConfigMap", Name: "cm"}, + } + if findDataVolumeOwnerRef(noRefs) != nil { + t.Error("expected nil for no DV ownerRef") + } + + // Empty slice + if findDataVolumeOwnerRef(nil) != nil { + t.Error("expected nil for nil refs") + } +} + +func TestFindVMOwnerRef(t *testing.T) { + refs := []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "my-dv"}, + {APIVersion: "kubevirt.io/v1", Kind: "VirtualMachine", Name: "my-vm"}, + } + + result := findVMOwnerRef(refs) + if result == nil { + t.Fatal("expected to find VirtualMachine ownerRef") + } + if result.Name != "my-vm" { + t.Errorf("expected name my-vm, got %s", result.Name) + } + + // No VM ownerRef + noVM := []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "my-dv"}, + } + if findVMOwnerRef(noVM) != nil { + t.Error("expected nil for no VM ownerRef") + } +} + +func TestRemoveDataVolumeOwnerRef(t *testing.T) { + refs := []metav1.OwnerReference{ + {APIVersion: "cdi.kubevirt.io/v1beta1", Kind: "DataVolume", Name: "my-dv"}, + {APIVersion: "v1", Kind: "ConfigMap", Name: "cm"}, + {APIVersion: "kubevirt.io/v1", Kind: "VirtualMachine", Name: "my-vm"}, + } + + result := removeDataVolumeOwnerRef(refs) + if len(result) != 2 { + t.Fatalf("expected 2 ownerRefs, got %d", len(result)) + } + for _, ref := range result { + if ref.Kind == "DataVolume" { + t.Error("DataVolume ownerRef should have been removed") + } + } + + // Empty refs + empty := removeDataVolumeOwnerRef(nil) + if len(empty) != 0 { + t.Errorf("expected 0 ownerRefs for nil input, got %d", len(empty)) + } +} + +func TestRemoveCDIAnnotations(t *testing.T) { + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "cdi.kubevirt.io/storage.condition.running": "true", + "cdi.kubevirt.io/storage.pod.phase": "Succeeded", + "replication.storage.dell.com/remotePV": "remote-pv", + "app.kubernetes.io/name": "my-app", + }, + }, + } + + removeCDIAnnotations(pvc) + + if len(pvc.Annotations) != 2 { + t.Errorf("expected 2 annotations after removal, got %d", len(pvc.Annotations)) + } + for key := range pvc.Annotations { + if strings.Contains(key, "cdi.kubevirt.io") { + t.Errorf("CDI annotation %s should have been removed", key) + } + } + + // Nil annotations — should not panic + nilPVC := &v1.PersistentVolumeClaim{} + removeCDIAnnotations(nilPVC) // no panic = pass +} + +func TestRecoverPVCBackup(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPersistentVolume := getPersistentVolume + + after := func() { + getPersistentVolume = originalGetPersistentVolume + } + + sc := "sc-1" + backupPVC := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-pvc", + Namespace: "ns", + Annotations: map[string]string{ + controllers.RemotePV: "remote-pv", + }, + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "local-pv", + StorageClassName: &sc, + }, + } + backup := &pvcSwapBackup{ + PVC: backupPVC, + LocalPVPolicy: v1.PersistentVolumeReclaimDelete, + RemotePVPolicy: v1.PersistentVolumeReclaimRetain, + } + backupJSON, _ := json.Marshal(backup) + + tests := []struct { + name string + setup func() + expectedErr bool + errContains string + }{ + { + name: "Error getting target PV", + setup: func() { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return nil, fmt.Errorf("not found") + } + }, + expectedErr: true, + errContains: "error getting target PV", + }, + { + name: "Target PV has no RemotePV annotation", + setup: func() { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{}, + }, + }, nil + } + }, + expectedErr: true, + errContains: "has no RemotePV annotation", + }, + { + name: "Error getting local PV", + setup: func() { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + return nil, fmt.Errorf("local PV not found") + } + }, + expectedErr: true, + errContains: "error getting local PV", + }, + { + name: "Local PV has no pending swap annotation", + setup: func() { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local-pv", + Annotations: map[string]string{}, + }, + }, nil + } + }, + expectedErr: true, + errContains: "no pending PVC swap annotation", + }, + { + name: "Invalid JSON in backup annotation", + setup: func() { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local-pv", + Annotations: map[string]string{ + controllers.PendingPVCSwap: "not-valid-json", + }, + }, + }, nil + } + }, + expectedErr: true, + errContains: "error unmarshaling PVC backup", + }, + { + name: "Successful recovery", + setup: func() { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local-pv", + Annotations: map[string]string{ + controllers.PendingPVCSwap: string(backupJSON), + }, + }, + }, nil + } + }, + expectedErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup() + defer after() + log := ctrl.Log.WithName("test") + result, err := recoverPVCBackup(context.Background(), nil, "target-pv", log) + if tt.expectedErr { + if err == nil { + t.Errorf("expected error, got nil") + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("expected error containing %q, got %q", tt.errContains, err.Error()) + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected non-nil backup") + } + if result.PVC.Name != "backup-pvc" { + t.Errorf("expected PVC name backup-pvc, got %s", result.PVC.Name) + } + if result.LocalPVPolicy != v1.PersistentVolumeReclaimDelete { + t.Errorf("expected LocalPVPolicy Delete, got %s", result.LocalPVPolicy) + } + } + }) + } +} + +func TestSavePVCBackupToPV(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalUpdatePV := updatePersistentVolume + defer func() { updatePersistentVolume = originalUpdatePV }() + + sc := "sc-1" + backup := &pvcSwapBackup{ + PVC: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "pvc", Namespace: "ns"}, + Spec: v1.PersistentVolumeClaimSpec{VolumeName: "pv", StorageClassName: &sc}, + }, + LocalPVPolicy: v1.PersistentVolumeReclaimDelete, + RemotePVPolicy: v1.PersistentVolumeReclaimRetain, + } + log := ctrl.Log.WithName("test") + + t.Run("Successful save", func(t *testing.T) { + localPV := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "local-pv"}, + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + err := savePVCBackupToPV(context.Background(), nil, localPV, backup, log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if localPV.Annotations[controllers.PendingPVCSwap] == "" { + t.Error("expected PendingPVCSwap annotation to be set") + } + if localPV.Spec.PersistentVolumeReclaimPolicy != v1.PersistentVolumeReclaimRetain { + t.Errorf("expected Retain policy, got %s", localPV.Spec.PersistentVolumeReclaimPolicy) + } + }) + + t.Run("Update error", func(t *testing.T) { + localPV := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "local-pv", Annotations: map[string]string{}}, + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return fmt.Errorf("update failed") + } + err := savePVCBackupToPV(context.Background(), nil, localPV, backup, log) + if err == nil { + t.Error("expected error") + } else if !strings.Contains(err.Error(), "error saving PVC backup") { + t.Errorf("expected 'error saving PVC backup', got %q", err.Error()) + } + }) + + t.Run("Nil annotations initialised", func(t *testing.T) { + localPV := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "local-pv"}, + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + err := savePVCBackupToPV(context.Background(), nil, localPV, backup, log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if localPV.Annotations == nil { + t.Error("expected annotations to be initialised") + } + }) +} + +func TestVerifyPVC(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPVC := getPersistentVolumeClaim + originalSleep := sleep + defer func() { + getPersistentVolumeClaim = originalGetPVC + sleep = originalSleep + }() + sleep = func(_ time.Duration) {} + + t.Run("Success on first attempt", func(t *testing.T) { + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + return &v1.PersistentVolumeClaim{ + Spec: v1.PersistentVolumeClaimSpec{VolumeName: "target-pv"}, + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + err := verifyPVC(context.Background(), nil, "target-pv", "local-pv", "pvc", "ns") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("Timeout when PVC never matches", func(t *testing.T) { + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + return &v1.PersistentVolumeClaim{ + Spec: v1.PersistentVolumeClaimSpec{VolumeName: "wrong-pv"}, + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + controllers.RemotePV: "wrong-remote", + }, + }, + }, nil + } + err := verifyPVC(context.Background(), nil, "target-pv", "local-pv", "pvc", "ns") + if err == nil { + t.Error("expected timeout error") + } else if !strings.Contains(err.Error(), "timed out") { + t.Errorf("expected 'timed out', got %q", err.Error()) + } + }) + + t.Run("Error fetching PVC retries then times out", func(t *testing.T) { + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + return nil, fmt.Errorf("transient error") + } + err := verifyPVC(context.Background(), nil, "target-pv", "local-pv", "pvc", "ns") + if err == nil { + t.Error("expected timeout error") + } else if !strings.Contains(err.Error(), "timed out") { + t.Errorf("expected 'timed out', got %q", err.Error()) + } + }) + + t.Run("Success on third attempt", func(t *testing.T) { + callCount := 0 + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + callCount++ + if callCount < 3 { + return &v1.PersistentVolumeClaim{ + Spec: v1.PersistentVolumeClaimSpec{VolumeName: "wrong-pv"}, + }, nil + } + return &v1.PersistentVolumeClaim{ + Spec: v1.PersistentVolumeClaimSpec{VolumeName: "target-pv"}, + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + err := verifyPVC(context.Background(), nil, "target-pv", "local-pv", "pvc", "ns") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) +} + +func TestUpdatePVClaimRefSuccess(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + defer func() { + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + }() + + t.Run("ClaimRef already set returns nil", func(t *testing.T) { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "existing", Namespace: "ns"}, + }, + }, nil + } + log := ctrl.Log.WithName("test") + err := updatePVClaimRef(context.Background(), nil, "pv", "ns", "rv", "pvc", "uid", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("Successful update clears RemotePVC annotations", func(t *testing.T) { + var updatedPV *v1.PersistentVolume + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + controllers.RemotePVCNamespace: "old-ns", + controllers.RemotePVC: "old-pvc", + }, + Labels: map[string]string{ + controllers.RemotePVCNamespace: "old-ns", + }, + }, + Spec: v1.PersistentVolumeSpec{}, + }, nil + } + // After update, return PV with ClaimRef set (simulates successful update) + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "pvc", Namespace: "ns"}, + }, + }, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, pv *v1.PersistentVolume) error { + updatedPV = pv + return nil + } + log := ctrl.Log.WithName("test") + err := updatePVClaimRef(context.Background(), nil, "pv", "ns", "rv", "pvc", "uid", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if updatedPV == nil { + t.Fatal("PV was not updated") + } + if updatedPV.Annotations[controllers.RemotePVCNamespace] != "" { + t.Errorf("expected RemotePVCNamespace annotation to be cleared") + } + if updatedPV.Labels[controllers.RemotePVCNamespace] != "" { + t.Errorf("expected RemotePVCNamespace label to be cleared") + } + if updatedPV.Annotations[controllers.RemotePVC] != "" { + t.Errorf("expected RemotePVC annotation to be cleared") + } + if updatedPV.Spec.ClaimRef == nil { + t.Error("expected ClaimRef to be set") + } + }) +} + +func TestRemovePVClaimRefSuccess(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + originalSleep := sleep + defer func() { + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + sleep = originalSleep + }() + sleep = func(_ time.Duration) {} + + t.Run("ClaimRef already nil returns immediately", func(t *testing.T) { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ClaimRef: nil}, + }, nil + } + log := ctrl.Log.WithName("test") + err := removePVClaimRef(context.Background(), nil, "pv", "ns", "pvc", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("ClaimRef removed on first update", func(t *testing.T) { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + Labels: map[string]string{}, + }, + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "pvc", Namespace: "ns"}, + }, + }, nil + } + return &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{ClaimRef: nil}}, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + log := ctrl.Log.WithName("test") + err := removePVClaimRef(context.Background(), nil, "pv", "ns", "pvc", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("Conflict retries then succeeds", func(t *testing.T) { + updateCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + Labels: map[string]string{}, + }, + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "pvc", Namespace: "ns"}, + }, + }, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + updateCount++ + if updateCount == 1 { + return k8serrors.NewConflict(schema.GroupResource{}, "pv", fmt.Errorf("conflict")) + } + return nil + } + log := ctrl.Log.WithName("test") + err := removePVClaimRef(context.Background(), nil, "pv", "ns", "pvc", log) + // It won't return nil because after update succeeds, it loops and getPV returns with ClaimRef again + // But this exercises the conflict retry path + if err != nil { + t.Logf("got expected error from retry loop: %v", err) + } + }) +} + +func TestRemoveReservedClaimRefSuccess(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + originalSleep := sleep + defer func() { + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + sleep = originalSleep + }() + sleep = func(_ time.Duration) {} + + t.Run("ClaimRef already nil", func(t *testing.T) { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{ClaimRef: nil}}, nil + } + log := ctrl.Log.WithName("test") + err := removeReservedClaimRefForTargetPV(context.Background(), nil, "pv", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("ClaimRef removed successfully", func(t *testing.T) { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "reserved", Namespace: "reserved"}, + }, + }, nil + } + return &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{ClaimRef: nil}}, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + log := ctrl.Log.WithName("test") + err := removeReservedClaimRefForTargetPV(context.Background(), nil, "pv", log) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("Conflict retry on update", func(t *testing.T) { + updateCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + ClaimRef: &v1.ObjectReference{Name: "reserved", Namespace: "reserved"}, + }, + }, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + updateCount++ + if updateCount == 1 { + return k8serrors.NewConflict(schema.GroupResource{}, "pv", fmt.Errorf("conflict")) + } + return nil + } + log := ctrl.Log.WithName("test") + err := removeReservedClaimRefForTargetPV(context.Background(), nil, "pv", log) + // Exercises the conflict branch + if err != nil { + t.Logf("got error from retry loop: %v", err) + } + }) +} + +func TestSetPVReclaimPolicySuccess(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + originalSleep := sleep + defer func() { + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + sleep = originalSleep + }() + sleep = func(_ time.Duration) {} + + t.Run("Policy set on first attempt", func(t *testing.T) { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimRetain, + }, + }, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + err := setPVReclaimPolicy(context.Background(), nil, "pv", v1.PersistentVolumeReclaimRetain) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("Second get error", func(t *testing.T) { + callCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + callCount++ + if callCount == 1 { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, + }, + }, nil + } + return nil, fmt.Errorf("error on re-read") + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + err := setPVReclaimPolicy(context.Background(), nil, "pv", v1.PersistentVolumeReclaimRetain) + if err == nil { + t.Error("expected error") + } else if !strings.Contains(err.Error(), "error retrieving PV") { + t.Errorf("expected 'error retrieving PV', got %q", err.Error()) + } + }) + + t.Run("Timeout when policy never sticks", func(t *testing.T) { + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, + }, + }, nil + } + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + err := setPVReclaimPolicy(context.Background(), nil, "pv", v1.PersistentVolumeReclaimRetain) + if err == nil { + t.Error("expected timeout error") + } else if !strings.Contains(err.Error(), "timed out") { + t.Errorf("expected 'timed out', got %q", err.Error()) + } + }) +} + +func TestSwapPVCRecoveryPath(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPVC := getPersistentVolumeClaim + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + originalCreatePVC := createPersistentVolumeClaim + originalSleep := sleep + + after := func() { + getPersistentVolumeClaim = originalGetPVC + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + createPersistentVolumeClaim = originalCreatePVC + sleep = originalSleep + } + defer after() + sleep = func(_ time.Duration) {} + + t.Run("PVC not found and recovery fails", func(t *testing.T) { + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + return nil, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumeclaims"}, "pvc") + } + // recoverPVCBackup calls getPersistentVolume + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ string) (*v1.PersistentVolume, error) { + return nil, fmt.Errorf("pv not found") + } + + r := &ReplicationGroupReconciler{ + Log: ctrl.Log.WithName("test"), + Domain: constants.DefaultDomain, + } + log := ctrl.Log.WithName("test") + err := r.swapPVC(context.Background(), nil, "pvc", "ns", "target-pv", "rg-target", log) + if err == nil { + t.Error("expected error") + } else if !strings.Contains(err.Error(), "recovery failed") { + t.Errorf("expected 'recovery failed', got %q", err.Error()) + } + }) + + t.Run("PVC not found but recovery succeeds then create fails", func(t *testing.T) { + sc := "sc-1" + remoteSC := "sc-2" + backupPVC := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + controllers.StorageClassRemoteStorageClassParam: remoteSC, + controllers.ReplicationGroup: "rg-old", + }, + Labels: map[string]string{ + controllers.ReplicationGroup: "rg-old", + }, + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "local-pv", + StorageClassName: &sc, + }, + } + backup := &pvcSwapBackup{ + PVC: backupPVC, + LocalPVPolicy: v1.PersistentVolumeReclaimDelete, + RemotePVPolicy: v1.PersistentVolumeReclaimRetain, + } + backupJSON, _ := json.Marshal(backup) + + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, _ string) (*v1.PersistentVolumeClaim, error) { + return nil, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumeclaims"}, "pvc") + } + + pvCallCount := 0 + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, name string) (*v1.PersistentVolume, error) { + pvCallCount++ + if pvCallCount == 1 { + // target PV for recoverPVCBackup + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "target-pv", + Annotations: map[string]string{ + controllers.RemotePV: "local-pv", + }, + }, + }, nil + } + if pvCallCount == 2 { + // local PV for recoverPVCBackup + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "local-pv", + Annotations: map[string]string{ + controllers.PendingPVCSwap: string(backupJSON), + }, + }, + }, nil + } + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + }, nil + } + + createPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolumeClaim) error { + return fmt.Errorf("create failed") + } + + r := &ReplicationGroupReconciler{ + Log: ctrl.Log.WithName("test"), + Domain: constants.DefaultDomain, + } + log := ctrl.Log.WithName("test") + err := r.swapPVC(context.Background(), nil, "pvc", "ns", "target-pv", "rg-target", log) + if err == nil { + t.Error("expected error") + } else if !strings.Contains(err.Error(), "unable to create PVC") { + t.Errorf("expected 'unable to create PVC', got %q", err.Error()) + } + }) +} + +func (suite *RGControllerTestSuite) TestProcessFailBackBothDisabled() { + rg, _, _, _, _ := suite.getSingleClusterPVSetup() + rgName := rg.Name + + // Disable both PVC remap and kubevirt PVC remap + suite.reconciler.DisablePVCRemap = true + suite.reconciler.EnableKubevirtPVCRemap = false + + // Invoke failback action + time := metav1.Now() + lastAction := repv1.LastAction{ + Time: &time, + Condition: "Action FAILBACK_LOCAL succeeded", + } + rg.Status = repv1.DellCSIReplicationGroupStatus{ + LastAction: lastAction, + Conditions: []repv1.LastAction{lastAction}, + } + rg.Annotations[controllers.ActionProcessedTime] = time.String() + + err := suite.client.Update(context.Background(), rg) + suite.NoError(err) + + req := suite.getTypicalRequest() + resp, err := suite.reconciler.Reconcile(context.Background(), req) + suite.NoError(err) + suite.Equal(false, resp.Requeue) + + // Verify PVC was NOT swapped because both remap flags are disabled + var unchangedPVC corev1.PersistentVolumeClaim + err = suite.client.Get(context.Background(), types.NamespacedName{Name: "fake-pvc", Namespace: "fake-ns"}, &unchangedPVC) + suite.NoError(err) + suite.Equal("local-pv", unchangedPVC.Spec.VolumeName, "PVC should still be bound to local PV") + suite.Equal(rgName, unchangedPVC.Annotations[controllers.ReplicationGroup], "RG annotation should be unchanged") +} + +func TestSwapPVCStaleClaimRef(t *testing.T) { + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + originalGetPVC := getPersistentVolumeClaim + originalGetPV := getPersistentVolume + originalUpdatePV := updatePersistentVolume + originalDeletePVC := deletePersistentVolumeClaim + originalCreatePVC := createPersistentVolumeClaim + originalSleep := sleep + + after := func() { + getPersistentVolumeClaim = originalGetPVC + getPersistentVolume = originalGetPV + updatePersistentVolume = originalUpdatePV + deletePersistentVolumeClaim = originalDeletePVC + createPersistentVolumeClaim = originalCreatePVC + sleep = originalSleep + } + defer after() + sleep = func(_ time.Duration) {} + + t.Run("Remote PV has stale claimRef - PVC not found removes it", func(t *testing.T) { + sc := "sc-1" + remoteSC := "sc-2" + pvc := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pvc", + Namespace: "fake-ns", + Annotations: map[string]string{ + controllers.RemotePV: "remote-pv", + controllers.StorageClassRemoteStorageClassParam: remoteSC, + controllers.ReplicationGroup: "rg-old", + }, + Labels: map[string]string{ + controllers.ReplicationGroup: "rg-old", + }, + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "local-pv", + StorageClassName: &sc, + }, + } + + pvcGetCount := 0 + getPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ string, name string) (*v1.PersistentVolumeClaim, error) { + pvcGetCount++ + if pvcGetCount == 1 { + return pvc, nil + } + if name == "stale-pvc" { + return nil, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumeclaims"}, "stale-pvc") + } + return nil, k8serrors.NewNotFound(schema.GroupResource{Resource: "persistentvolumeclaims"}, name) + } + + getPersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, name string) (*v1.PersistentVolume, error) { + if name == "local-pv" { + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "local-pv"}, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, + }, + }, nil + } + return &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{Name: "remote-pv"}, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimRetain, + ClaimRef: &v1.ObjectReference{ + Name: "stale-pvc", + Namespace: "stale-ns", + }, + }, + }, nil + } + + updatePersistentVolume = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolume) error { + return nil + } + deletePersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolumeClaim) error { + return nil + } + createPersistentVolumeClaim = func(_ context.Context, _ connection.RemoteClusterClient, _ *v1.PersistentVolumeClaim) error { + return fmt.Errorf("create failed") + } + + r := &ReplicationGroupReconciler{ + Log: ctrl.Log.WithName("test"), + Domain: constants.DefaultDomain, + } + log := ctrl.Log.WithName("test") + err := r.swapPVC(context.Background(), nil, "fake-pvc", "fake-ns", "remote-pv", "rg-target", log) + // We expect it to get past the stale ClaimRef check, then fail at create + if err == nil { + t.Error("expected error") + } else if !strings.Contains(err.Error(), "unable to create PVC") { + t.Errorf("expected 'unable to create PVC', got %q", err.Error()) + } + }) +} diff --git a/controllers/replication-controller/persistentvolume_controller.go b/controllers/replication-controller/persistentvolume_controller.go index 3c10d9c3..6eb204df 100644 --- a/controllers/replication-controller/persistentvolume_controller.go +++ b/controllers/replication-controller/persistentvolume_controller.go @@ -129,7 +129,16 @@ func (r *PersistentVolumeReconciler) Reconcile(ctx context.Context, req ctrl.Req // Process deletion of remote PV if _, ok := volume.Annotations[controller.DeletionRequested]; !ok { log.V(logger.InfoLevel).Info("Deletion requested annotation not found") - remoteVolume, err := rClient.GetPersistentVolume(ctx, volume.Annotations[controller.RemotePV]) + remotePVName := volume.Annotations[controller.RemotePV] + if remotePVName == "" { + log.V(logger.InfoLevel).Info("RemotePV annotation is empty, skipping remote volume lookup") + } + remoteVolume, err := func() (*v1.PersistentVolume, error) { + if remotePVName == "" { + return nil, &errors.StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonNotFound}} + } + return rClient.GetPersistentVolume(ctx, remotePVName) + }() if err != nil { // If remote PV doesn't exist, proceed to removing finalizer if !errors.IsNotFound(err) { @@ -300,7 +309,8 @@ func (r *PersistentVolumeReconciler) Reconcile(ctx context.Context, req ctrl.Req var resourceRequests []byte - if volume.Spec.ClaimRef != nil { + if volume.Spec.ClaimRef != nil && + !(volume.Spec.ClaimRef.Name == controller.ReservedPVCName && volume.Spec.ClaimRef.Namespace == controller.ReservedPVCNamespace) { pvc := new(v1.PersistentVolumeClaim) if err := r.Get(ctx, client.ObjectKey{ Namespace: volume.Spec.ClaimRef.Namespace, @@ -617,7 +627,7 @@ func UpdateRemotePVDetails(ctx context.Context, client connection.RemoteClusterC // Update the remote PV claimref if it differs from the local PV if volume.Spec.ClaimRef != nil && remoteClusterID != controller.Self { if remotePV.Spec.ClaimRef != nil && volume.Spec.ClaimRef.Name != remotePV.Spec.ClaimRef.Name { - log.V(logger.InfoLevel).Info(fmt.Sprintf("Remote PV claimref differs from the local PV claimref. Hence updating remote PV")) + log.V(logger.InfoLevel).Info("Remote PV claimref differs from the local PV claimref. Hence updating remote PV") remotePV.Spec.ClaimRef.Namespace = volume.Spec.ClaimRef.Namespace remotePV.Spec.ClaimRef.Name = volume.Spec.ClaimRef.Name remotePV.Spec.ClaimRef.ResourceVersion = volume.Spec.ClaimRef.ResourceVersion diff --git a/controllers/replication-controller/persistentvolumeclaim_controller.go b/controllers/replication-controller/persistentvolumeclaim_controller.go index 34480104..a5ef4131 100644 --- a/controllers/replication-controller/persistentvolumeclaim_controller.go +++ b/controllers/replication-controller/persistentvolumeclaim_controller.go @@ -149,9 +149,15 @@ func (r *PersistentVolumeClaimReconciler) Reconcile(ctx context.Context, req ctr remotePVCName := "" remotePVCNamespace := "" if remoteClusterID != controller.Self && r.AllowPVCCreationOnTarget { + // Skip remote PVC creation if local PVC is being deleted (e.g. temporary scratch PVCs) + if claim.DeletionTimestamp != nil { + log.V(logger.InfoLevel).Info("Local PVC is being deleted, skipping remote PVC creation") + return ctrl.Result{}, nil + } // if its not single cluster then check the pv status and create pvc on target cluster if remotePV.Status.Phase == v1.VolumeAvailable && remotePV.Spec.ClaimRef != nil { remoteClaim.Spec.AccessModes = remotePV.Spec.AccessModes + remoteClaim.Spec.VolumeMode = remotePV.Spec.VolumeMode remoteClaim.Spec.Resources.Requests = v1.ResourceList{ v1.ResourceStorage: remotePV.Spec.Capacity[v1.ResourceStorage], } @@ -167,8 +173,12 @@ func (r *PersistentVolumeClaimReconciler) Reconcile(ctx context.Context, req ctr // creating PVC on target err = rClient.CreatePersistentVolumeClaim(ctx, remoteClaim) if err != nil { - log.Error(err, "Failed to create remote PVC on target cluster") - return ctrl.Result{}, err + if errors.IsAlreadyExists(err) { + log.V(logger.InfoLevel).Info("Remote PVC already exists on target cluster") + } else { + log.Error(err, "Failed to create remote PVC on target cluster") + return ctrl.Result{}, err + } } } } @@ -305,10 +315,9 @@ func updatePVCAnnotationsAndSpec(pvc *v1.PersistentVolumeClaim, remoteClusterID controller.AddAnnotation(pvc, controller.PVCProtectionComplete, pvcProtectionComplete) // Created By controller.AddAnnotation(pvc, controller.CreatedBy, constants.DellReplicationController) - // Remote PV Name - remoteVolume, _ := getValueFromAnnotations(controller.RemoteVolumeAnnotation, pv.Annotations) - pvc.Spec.VolumeName = remoteVolume - controller.AddAnnotation(pvc, controller.RemotePV, remoteVolume) + // Remote PV Name - use PV name directly for pre-binding to the target PV + pvc.Spec.VolumeName = pv.Name + controller.AddAnnotation(pvc, controller.RemotePV, pv.Name) // Remote ClusterID controller.AddAnnotation(pvc, controller.RemoteClusterID, remoteClusterID) // Replication group @@ -316,9 +325,6 @@ func updatePVCAnnotationsAndSpec(pvc *v1.PersistentVolumeClaim, remoteClusterID // Remote Storage Class pvc.Spec.StorageClassName = &pv.Spec.StorageClassName controller.AddAnnotation(pvc, controller.RemoteStorageClassAnnotation, pv.Spec.StorageClassName) - // Remote Volume - remoteVolume, _ = getValueFromAnnotations(controller.RemoteVolumeAnnotation, pv.Annotations) - controller.AddAnnotation(pvc, controller.RemoteVolumeAnnotation, remoteVolume) // remote PVC namespace remotePVCNamespace, _ := getValueFromAnnotations(controller.RemotePVCNamespace, pv.Annotations) pvc.Namespace = remotePVCNamespace diff --git a/controllers/replication-controller/persistentvolumeclaim_controller_test.go b/controllers/replication-controller/persistentvolumeclaim_controller_test.go index 900ec6b0..12ed8438 100644 --- a/controllers/replication-controller/persistentvolumeclaim_controller_test.go +++ b/controllers/replication-controller/persistentvolumeclaim_controller_test.go @@ -553,10 +553,9 @@ func (suite *PVControllerTestSuite) TestAllowPVCCreationOnTarget_CreatesRemotePV if pvObj.Annotations == nil { pvObj.Annotations = make(map[string]string) } - // Add required annotations so updatePVCAnnotations can fill PVC metadata + // Add required annotations so updatePVCAnnotations can fill PVC metadata. pvObj.Annotations[controllers.ContextPrefix] = suite.driver.DriverName pvObj.Annotations[controllers.PVCProtectionComplete] = "yes" - pvObj.Annotations[controllers.RemoteVolumeAnnotation] = pvObj.Name pvObj.Annotations[controllers.RemotePVCNamespace] = suite.driver.Namespace pvObj.Annotations[controllers.RemotePVC] = "allow-create-pvc-remote" // Add required labels for updatePVCLabels @@ -565,6 +564,9 @@ func (suite *PVControllerTestSuite) TestAllowPVCCreationOnTarget_CreatesRemotePV } pvObj.Labels[controllers.DriverName] = suite.driver.DriverName pvObj.Labels[controllers.ReplicationGroup] = "rg0" + // Set VolumeMode to Block to verify it is propagated to the remote PVC + blockMode := corev1.PersistentVolumeBlock + pvObj.Spec.VolumeMode = &blockMode // Create the remote PV in the fake remote cluster err = remoteClient.CreatePersistentVolume(ctx, &pvObj) assert.Nil(suite.T(), err, "expected no error creating remote PV on target cluster") @@ -631,6 +633,11 @@ func (suite *PVControllerTestSuite) TestAllowPVCCreationOnTarget_CreatesRemotePV assert.Equal(suite.T(), suite.driver.DriverName, remotePVC.Labels[controllers.DriverName], "DriverName label") assert.Equal(suite.T(), "remote-123", remotePVC.Labels[controllers.RemoteClusterID], "RemoteClusterID label") assert.Equal(suite.T(), "rg0", remotePVC.Labels[controllers.ReplicationGroup], "ReplicationGroup label") + + // Verify VolumeMode is propagated from remote PV to remote PVC + if assert.NotNil(suite.T(), remotePVC.Spec.VolumeMode, "VolumeMode should be set on remote PVC") { + assert.Equal(suite.T(), corev1.PersistentVolumeBlock, *remotePVC.Spec.VolumeMode, "VolumeMode should be Block") + } } func TestUpdatePVCLabels(t *testing.T) { @@ -701,8 +708,8 @@ func TestUpdatePVCAnnotations(t *testing.T) { assert.Equal(t, "ctxPrefixVal", pvc.Annotations[controllers.ContextPrefix], "ContextPrefix annotation") assert.Equal(t, "complete", pvc.Annotations[controllers.PVCProtectionComplete], "PVCProtectionComplete annotation") assert.Equal(t, constants.DellReplicationController, pvc.Annotations[controllers.CreatedBy], "CreatedBy annotation") - assert.Equal(t, "remote-vol-id", pvc.Spec.VolumeName, "PVC Spec.VolumeName should be set to remote volume ID") - assert.Equal(t, "remote-vol-id", pvc.Annotations[controllers.RemotePV], "RemotePV annotation") + assert.Equal(t, "pv1", pvc.Spec.VolumeName, "PVC Spec.VolumeName should be set to PV name for pre-binding") + assert.Equal(t, "pv1", pvc.Annotations[controllers.RemotePV], "RemotePV annotation should be PV name") assert.Equal(t, "remote-123", pvc.Annotations[controllers.RemoteClusterID], "RemoteClusterID annotation") assert.Equal(t, "rg-1", pvc.Annotations[controllers.ReplicationGroup], "ReplicationGroup annotation") // StorageClassName is a pointer; compare the string value @@ -725,6 +732,76 @@ func TestUpdatePVCAnnotations(t *testing.T) { } } +func (suite *PVControllerTestSuite) TestAllowPVCCreationOnTarget_SkipsDeletingPVC() { + ctx := context.Background() + remoteClient, err := suite.fakeConfig.GetConnection("remote-123") + assert.Nil(suite.T(), err) + controllers.InitLabelsAndAnnotations(constants.DefaultDomain) + + // Set up a remote PV in the target cluster with Phase=Available and a ClaimRef + pvObj := suite.getPV("pv-skip-deleting") + pvObj.Status.Phase = corev1.VolumeAvailable + pvObj.Spec.ClaimRef = &corev1.ObjectReference{ + Kind: "PersistentVolumeClaim", + Namespace: suite.driver.Namespace, + Name: "deleting-pvc", + APIVersion: "v1", + } + if pvObj.Annotations == nil { + pvObj.Annotations = make(map[string]string) + } + pvObj.Annotations[controllers.ContextPrefix] = suite.driver.DriverName + pvObj.Annotations[controllers.PVCProtectionComplete] = "yes" + pvObj.Annotations[controllers.RemotePVCNamespace] = suite.driver.Namespace + pvObj.Annotations[controllers.RemotePVC] = "deleting-pvc-remote" + if pvObj.Labels == nil { + pvObj.Labels = make(map[string]string) + } + pvObj.Labels[controllers.DriverName] = suite.driver.DriverName + pvObj.Labels[controllers.ReplicationGroup] = "rg0" + err = remoteClient.CreatePersistentVolume(ctx, &pvObj) + assert.Nil(suite.T(), err) + + // Set up a local PVC that is being deleted (DeletionTimestamp set) + pvcObj := utils.GetPVCObj("deleting-pvc", suite.driver.Namespace, suite.driver.StorageClass) + pvcAnnotations := make(map[string]string) + pvcAnnotations[controllers.RemoteClusterID] = "remote-123" + pvcObj.Status.Phase = corev1.ClaimBound + pvcObj.Spec.VolumeName = pvObj.Name + pvcObj.Annotations = pvcAnnotations + pvcObj.Finalizers = []string{"test-finalizer"} + err = suite.client.Create(ctx, pvcObj) + assert.Nil(suite.T(), err) + + // Mark the PVC for deletion + err = suite.client.Delete(ctx, pvcObj) + assert.Nil(suite.T(), err) + + // Perform reconciliation with AllowPVCCreationOnTarget = true + pvcReq := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: suite.driver.Namespace, + Name: "deleting-pvc", + }, + } + fakeRecorder := record.NewFakeRecorder(100) + externalReconcile := PersistentVolumeClaimReconciler{ + Client: suite.client, + Log: ctrl.Log.WithName("controllers").WithName("DellCSIReplicationGroup"), + Scheme: utils.Scheme, + EventRecorder: fakeRecorder, + Config: suite.fakeConfig, + AllowPVCCreationOnTarget: true, + } + res, err := externalReconcile.Reconcile(ctx, pvcReq) + assert.Nil(suite.T(), err, "expected no error on PVC reconcile for deleting PVC") + assert.False(suite.T(), res.Requeue, "expected no requeue") + + // Verify that NO remote PVC was created on the target cluster + _, err = remoteClient.GetPersistentVolumeClaim(ctx, suite.driver.Namespace, "deleting-pvc-remote") + assert.NotNil(suite.T(), err, "expected remote PVC to NOT be created for a deleting PVC") +} + func TestVerifyNamespaceExistence_NamespaceAlreadyExists(t *testing.T) { // Setup fake multi-cluster config and get remote client fakeConfig := mocks.New("sourceCluster", "remote-123") diff --git a/core/semver/semver_test.go b/core/semver/semver_test.go index a2cd28f3..be5c6a7a 100644 --- a/core/semver/semver_test.go +++ b/core/semver/semver_test.go @@ -271,7 +271,7 @@ func TestErrorExit(t *testing.T) { return } // call the test again with INVOKE_ERROR_EXIT=1 so the errorExit function is invoked and we can check the return code - cmd := exec.Command(os.Args[0], "-test.run=TestErrorExit") // #nosec G204 + cmd := exec.Command(os.Args[0], "-test.run=TestErrorExit") // #nosec G204,G702 -- test-only: re-invoking test binary, not user input cmd.Env = append(os.Environ(), "INVOKE_ERROR_EXIT=1") stderr, err := cmd.StderrPipe() diff --git a/deploy/controller.yaml b/deploy/controller.yaml index 41871cb3..c2b17484 100644 --- a/deploy/controller.yaml +++ b/deploy/controller.yaml @@ -150,6 +150,9 @@ rules: - apiGroups: ["snapshot.storage.k8s.io"] resources: ["volumesnapshots"] verbs: ["get", "list", "watch", "update", "create", "delete"] + - apiGroups: ["cdi.kubevirt.io"] + resources: ["datavolumes"] + verbs: ["get", "update", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -295,6 +298,7 @@ spec: containers: - args: - --disable-pvc-remap=false + - --enable-kubevirt-pvc-remap=false - --allow-pvc-creation-on-target=false - --enable-leader-election - --prefix=replication.storage.dell.com @@ -319,7 +323,7 @@ spec: value: /app/certs - name: X_CSI_REPLICATION_CONFIG_FILE_NAME value: config - image: quay.io/dell/container-storage-modules/dell-replication-controller:v1.13.0 + image: quay.io/dell/container-storage-modules/dell-replication-controller:v1.15.0 imagePullPolicy: IfNotPresent name: manager resources: diff --git a/deploy/role.yaml b/deploy/role.yaml index efde5a47..87cac629 100644 --- a/deploy/role.yaml +++ b/deploy/role.yaml @@ -119,6 +119,14 @@ rules: - get - list - watch + - apiGroups: + - cdi.kubevirt.io + resources: + - datavolumes + verbs: + - get + - update + - delete --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role diff --git a/go.mod b/go.mod index 41d336f8..cc772481 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/dell/csm-replication -go 1.25 +go 1.26 require ( - github.com/dell/dell-csi-extensions/common v1.10.0 - github.com/dell/dell-csi-extensions/migration v1.10.0 - github.com/dell/dell-csi-extensions/replication v1.13.0 - github.com/dell/gobrick v1.16.0 + github.com/dell/dell-csi-extensions/common v1.11.0 + github.com/dell/dell-csi-extensions/migration v1.11.0 + github.com/dell/dell-csi-extensions/replication v1.14.0 + github.com/dell/gobrick v1.17.0 github.com/bombsimon/logrusr/v4 v4.1.0 - github.com/fatih/color v1.18.0 + github.com/fatih/color v1.19.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-chi/chi v4.1.2+incompatible github.com/go-logr/logr v1.4.3 @@ -18,20 +18,19 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.19.0 - google.golang.org/grpc v1.77.0 - google.golang.org/protobuf v1.36.10 - k8s.io/api v0.34.2 - k8s.io/apiextensions-apiserver v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/client-go v0.34.2 - sigs.k8s.io/controller-runtime v0.22.4 + golang.org/x/sync v0.20.0 + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 + k8s.io/api v0.35.3 + k8s.io/apiextensions-apiserver v0.35.3 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( - github.com/dell/goiscsi v1.14.0 // indirect - github.com/dell/gonvme v1.13.0 // indirect + github.com/dell/goiscsi v1.15.0 // indirect + github.com/dell/gonvme v1.14.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -55,7 +54,6 @@ require ( github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect @@ -83,13 +81,14 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401001100-f93e5f3e9f0f // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -98,6 +97,6 @@ require ( k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index af28903f..26a9a8ea 100644 --- a/go.sum +++ b/go.sum @@ -11,18 +11,18 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/dell/dell-csi-extensions/common v1.10.0 h1:WIFPWVEBUyzOTCOPAlcgQsiRRGAufyKJYbIATbNZXIY= -github.com/dell/dell-csi-extensions/common v1.10.0/go.mod h1:zRHzmPX5SQQnqQ1LEIxG4hYqLBeQOSiD8TkEhU0eWTY= -github.com/dell/dell-csi-extensions/migration v1.10.0 h1:TwMCI91zZNuDUqr3TcL1BCWL1cNkQMwkupyRbVbYJAo= -github.com/dell/dell-csi-extensions/migration v1.10.0/go.mod h1:BN7Mlxt3CrpqrxxliH3BadAtmAyTzdBKDid7IDyfoi4= -github.com/dell/dell-csi-extensions/replication v1.13.0 h1:DSpoZ3vX65a3KDxUv0OinLkY2qUAQtRX3E1c1e3fnvA= -github.com/dell/dell-csi-extensions/replication v1.13.0/go.mod h1:aJBwd55amqbY3kk8SG7NjwH7nxBscceDwc1rKesUG1g= -github.com/dell/gobrick v1.16.0 h1:z/a9qXnT3hx3D4I+SJUMnIgJtcCx0j3gzmPPDUWtoYs= -github.com/dell/gobrick v1.16.0/go.mod h1:9uoH8EsNi9yAsUZj2gZFgB5kqdlyvArqx0tYC7Qg9IM= -github.com/dell/goiscsi v1.14.0 h1:kNDqOlpJ3cLSJh7Hfyn/Kz/FMCKHzV0s/xx4EqnelFw= -github.com/dell/goiscsi v1.14.0/go.mod h1:SCSC8dJCqTosU7SspaoLv6ICTKNEz08rt/I8nZ3+ptc= -github.com/dell/gonvme v1.13.0 h1:j8A1BzYA48gelih3xWd/J6LQ71CbC8Lbdyv0jG8uUNU= -github.com/dell/gonvme v1.13.0/go.mod h1:L5K7V4JZTf12m3k2wdwKwP+/eA6pr8DvlCsJU1QTGOQ= +github.com/dell/dell-csi-extensions/common v1.11.0 h1:G3chBDrKZN61XFJwY42AMQ9Rd7uIYrIoktRb//2pGEs= +github.com/dell/dell-csi-extensions/common v1.11.0/go.mod h1:zjvMAs9sJ6rxDKsNShd/TxZVmiQDMkZ6jIyOuaNADTQ= +github.com/dell/dell-csi-extensions/migration v1.11.0 h1:e8gjMH0zNUFXLsTExKDNbnd0xfj////ICrnR70DJ7ko= +github.com/dell/dell-csi-extensions/migration v1.11.0/go.mod h1:U0TxEJYo6RoTOJwMp2VMm8Fg4xi9FXJuJy+LSQnDsPA= +github.com/dell/dell-csi-extensions/replication v1.14.0 h1:9ZlrHTO5AzvH2QNahPW+1acXPiVBE6G3OoveYTebV9I= +github.com/dell/dell-csi-extensions/replication v1.14.0/go.mod h1:dM5xa34Qt3nN5I1GczwFrLcc0RB5lQVVOPEWKp5ZIZU= +github.com/dell/gobrick v1.17.0 h1:bfbNMhHmoiDLLrQbAlXxbSwCYp8RXO85OXKKF+XkpcM= +github.com/dell/gobrick v1.17.0/go.mod h1:omT0QLeai8b7NP+e/bQLUcuhaRQiKDgZQX6TOfweaLg= +github.com/dell/goiscsi v1.15.0 h1:71QzLLm4X8XrEkGLnZshpGEDdkgbFuZ8NiwARFwaCtY= +github.com/dell/goiscsi v1.15.0/go.mod h1:jlkRplXgeJHMZZ/dLUkWAnNcOrkIXxuibi9vDbPKYk4= +github.com/dell/gonvme v1.14.0 h1:dRyS0o+3B+cnnncgblb/H0qUJkNzjkPAq/82oqt/eMc= +github.com/dell/gonvme v1.14.0/go.mod h1:bx/tqYBKuY8SHxEpw9b8SiD/98+4TQdMYkYWES39Dgw= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= @@ -36,6 +36,8 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -71,8 +73,8 @@ github.com/evanphx/json-patch v5.9.10+incompatible h1:f9RK4b5sgikwA7D5BkGR9oz69K github.com/evanphx/json-patch v5.9.10+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -143,8 +145,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -186,8 +186,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -208,7 +208,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -251,12 +250,12 @@ github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -274,8 +273,8 @@ github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwk github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -305,7 +304,6 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -313,16 +311,16 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -365,6 +363,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -382,13 +382,12 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -402,12 +401,11 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -423,7 +421,6 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -433,13 +430,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -447,8 +444,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -477,21 +474,19 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -512,16 +507,16 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401001100-f93e5f3e9f0f h1:Rka45QInERYknkHYfJEPBQaoobXl+YpxTMjAKgWUq2A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401001100-f93e5f3e9f0f/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -531,8 +526,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -557,16 +552,16 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= @@ -581,15 +576,15 @@ k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= -sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= -sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/helper.mk b/helper.mk index fb9b2394..c3e47f56 100644 --- a/helper.mk +++ b/helper.mk @@ -9,7 +9,7 @@ gen-semver: go run core/semver/semver.go -f mk > semver.mk download-csm-common: - git clone --depth 1 git@github.com:CSM/csm.git temp-repo + git clone --depth 1 git@github.com:dell/csm.git temp-repo cp temp-repo/config/csm-common.mk . rm -rf temp-repo diff --git a/images.mk b/images.mk index 42937bfd..3f0ff043 100644 --- a/images.mk +++ b/images.mk @@ -9,13 +9,13 @@ include helper.mk CONTROLLER_IMAGE_NAME=dell-replication-controller REPLICATOR_IMAGE_NAME=dell-csi-replicator -CONTROLLER_REPLICATOR_VERSION=1.14.0 +CONTROLLER_REPLICATOR_VERSION=1.15.0 MIGRATOR_IMAGE_NAME=dell-csi-migrator -MIGRATOR_VERSION=1.10.0 +MIGRATOR_VERSION=1.11.0 RESCANNER_IMAGE_NAME=dell-csi-node-rescanner -RESCANNER_VERSION=1.9.0 +RESCANNER_VERSION=1.10.0 .PHONY: eval diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index e07f76c9..833b0e2b 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1089,7 +1089,7 @@ CSI_LOG_LEVEL: "INFO"`) // 3. Defer the restore operation defer func() { - err := os.WriteFile(configFilePath, originalContent, 0o600) + err := os.WriteFile(configFilePath, originalContent, 0o600) // #nosec G703 -- test-only: writing to temp test config file if err != nil { t.Errorf("Failed to restore original config content: %v", err) } diff --git a/pkg/connection/interface.go b/pkg/connection/interface.go index 89474f4b..653a34fe 100644 --- a/pkg/connection/interface.go +++ b/pkg/connection/interface.go @@ -50,6 +50,9 @@ type RemoteClusterClient interface { GetSnapshotClass(ctx context.Context, snapClassName string) (*s1.VolumeSnapshotClass, error) CreateNamespace(ctx context.Context, content *corev1.Namespace) error GetNamespace(ctx context.Context, namespace string) (*corev1.Namespace, error) + GetObject(ctx context.Context, key ctrlClient.ObjectKey, obj ctrlClient.Object) error + UpdateObject(ctx context.Context, obj ctrlClient.Object) error + DeleteObject(ctx context.Context, obj ctrlClient.Object) error } // ConnHandler - Interface diff --git a/pkg/connection/k8sconnections.go b/pkg/connection/k8sconnections.go index 963a0e1a..20f2b035 100644 --- a/pkg/connection/k8sconnections.go +++ b/pkg/connection/k8sconnections.go @@ -329,6 +329,21 @@ func (c *RemoteK8sControllerClient) GetNamespace(ctx context.Context, namespace return found, nil } +// GetObject fetches a generic Kubernetes object (including unstructured CRDs) from the cluster. +func (c *RemoteK8sControllerClient) GetObject(ctx context.Context, key ctrlClient.ObjectKey, obj ctrlClient.Object) error { + return c.Client.Get(ctx, key, obj) +} + +// UpdateObject updates a generic Kubernetes object (including unstructured CRDs) on the cluster. +func (c *RemoteK8sControllerClient) UpdateObject(ctx context.Context, obj ctrlClient.Object) error { + return c.Client.Update(ctx, obj) +} + +// DeleteObject deletes a generic Kubernetes object (including unstructured CRDs) from the cluster. +func (c *RemoteK8sControllerClient) DeleteObject(ctx context.Context, obj ctrlClient.Object) error { + return c.Client.Delete(ctx, obj) +} + // GetControllerClient - Returns a controller client which reads and writes directly to API server func GetControllerClient(restConfig *rest.Config, scheme *runtime.Scheme) (ctrlClient.Client, error) { // Create a temp client and use it diff --git a/repctl/cmd/repctl/main.go b/repctl/cmd/repctl/main.go index bd075976..f05a05ec 100644 --- a/repctl/cmd/repctl/main.go +++ b/repctl/cmd/repctl/main.go @@ -62,7 +62,7 @@ func setupRepctlCommand() *cobra.Command { Use: "repctl", Short: "repctl is CLI tool for managing replication in Kubernetes", Long: "repctl is CLI tool for managing replication in Kubernetes", - Version: "v1.13.0", + Version: "v1.15.0", PersistentPreRun: func(cmd *cobra.Command, args []string) { metadata.Init(viper.GetString(config.ReplicationPrefix)) }, diff --git a/repctl/cmd/repctl/main_test.go b/repctl/cmd/repctl/main_test.go index 65c11598..ab943be9 100644 --- a/repctl/cmd/repctl/main_test.go +++ b/repctl/cmd/repctl/main_test.go @@ -34,7 +34,6 @@ func TestSetupRepctlCommand(t *testing.T) { assert.Equal(t, "repctl", repctl.Use) assert.Equal(t, "repctl is CLI tool for managing replication in Kubernetes", repctl.Short) assert.Equal(t, "repctl is CLI tool for managing replication in Kubernetes", repctl.Long) - assert.Equal(t, "v1.13.0", repctl.Version) // Assert the persistent flags clustersFlag := repctl.PersistentFlags().Lookup("clusters") diff --git a/repctl/go.mod b/repctl/go.mod index a088eed5..b896a9d9 100644 --- a/repctl/go.mod +++ b/repctl/go.mod @@ -1,11 +1,11 @@ module github.com/dell/repctl -go 1.25 +go 1.26 replace github.com/dell/csm-replication => ../ require ( - github.com/dell/csm-replication v1.13.1-0.20260120071306-385dc945a79d + github.com/dell/csm-replication v1.14.0 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 @@ -16,11 +16,11 @@ require ( github.com/vektra/mockery/v2 v2.53.5 github.com/x-cray/logrus-prefixed-formatter v0.5.2 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.34.2 - k8s.io/apiextensions-apiserver v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/client-go v0.34.2 - sigs.k8s.io/controller-runtime v0.22.4 + k8s.io/api v0.35.3 + k8s.io/apiextensions-apiserver v0.35.3 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 + sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/yaml v1.6.0 ) @@ -47,7 +47,6 @@ require ( github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/huandu/xstrings v1.4.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect @@ -75,17 +74,17 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -94,5 +93,5 @@ require ( k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) diff --git a/repctl/go.sum b/repctl/go.sum index 0b5864bb..df3900b3 100644 --- a/repctl/go.sum +++ b/repctl/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -67,8 +69,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -86,8 +86,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -101,8 +101,6 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -135,12 +133,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -157,8 +155,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4 github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -194,7 +192,6 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJ github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -208,31 +205,28 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -249,25 +243,23 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -278,8 +270,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -298,27 +290,27 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= -sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= -sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/repctl/pkg/cmd/edit.go b/repctl/pkg/cmd/edit.go index ab334fcb..43d167f0 100644 --- a/repctl/pkg/cmd/edit.go +++ b/repctl/pkg/cmd/edit.go @@ -77,7 +77,7 @@ func (s *Secret) ToDecodedSecret() *DecodedSecret { m[k] = string(v) } - meta := metav1.TypeMeta{"Secret", "v1"} + meta := metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"} objectMeta := metav1.ObjectMeta{ Name: s.ObjectMeta.Name, Namespace: s.ObjectMeta.Namespace, @@ -180,7 +180,7 @@ func editSecretCommand() *cobra.Command { if !existence { editor = "vi" } - command := exec.Command(editor, tmpFile.Name()) // #nosec G204 --neither editor nor tmpFile.Name() can be hardcoded + command := exec.Command(editor, tmpFile.Name()) // #nosec G702 --neither editor nor tmpFile.Name() can be hardcoded command.Stdout = os.Stdout command.Stderr = os.Stderr command.Stdin = os.Stdin diff --git a/scripts/install.sh b/scripts/install.sh index af972228..de1d9f6d 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -21,7 +21,10 @@ MODE="install" NS="dell-replication-controller" RELEASE="replication" MODULE="csm-replication" -HELMCHARTVERSION="csm-replication-1.13.0" +DEFAULT_VERSION="v1.15.0" + +# Derive HELMCHARTVERSION from DEFAULT_VERSION (single source of truth) +HELMCHARTVERSION="${MODULE}-${DEFAULT_VERSION#v}" # export the name of the debug log, so child processes will see it export DEBUGLOG="${SCRIPTDIR}/install-debug.log" diff --git a/test/e2e-framework/fake-client/fake-client.go b/test/e2e-framework/fake-client/fake-client.go index a4bc2056..7f66f012 100644 --- a/test/e2e-framework/fake-client/fake-client.go +++ b/test/e2e-framework/fake-client/fake-client.go @@ -411,6 +411,11 @@ func (f SubResourceClient) Patch(_ context.Context, _ client.Object, _ client.Pa panic("implement me") } +// Apply applies the given configuration to the subresource. +func (f SubResourceClient) Apply(_ context.Context, _ runtime.ApplyConfiguration, _ ...client.SubResourceApplyOption) error { + panic("implement me") +} + // Get retrieves a subResource for the given obj object from the Kubernetes Cluster. // TODO: Implement func (f SubResourceClient) Get(_ context.Context, _ client.Object, _ client.Object, _ ...client.SubResourceGetOption) error { diff --git a/test/mock-server/server/server.go b/test/mock-server/server/server.go index 1bb06bda..79a77e4b 100644 --- a/test/mock-server/server/server.go +++ b/test/mock-server/server/server.go @@ -18,7 +18,7 @@ package server import ( "bytes" - context2 "context" + "context" "encoding/json" "fmt" "io" @@ -28,14 +28,13 @@ import ( commonext "github.com/dell/dell-csi-extensions/common" "github.com/dell/dell-csi-extensions/replication" - "golang.org/x/net/context" ) // Replication mock controller that implements replication related calls type Replication struct{} // VolumeMigrate - mocks Migrate function -func (s *Replication) VolumeMigrate(_ context2.Context, _ *migration.VolumeMigrateRequest) (*migration.VolumeMigrateResponse, error) { +func (s *Replication) VolumeMigrate(_ context.Context, _ *migration.VolumeMigrateRequest) (*migration.VolumeMigrateResponse, error) { rep := &migration.VolumeMigrateResponse{ MigratedVolume: &migration.Volume{ CapacityBytes: 3221225472, @@ -56,7 +55,7 @@ func (s *Replication) VolumeMigrate(_ context2.Context, _ *migration.VolumeMigra } // GetMigrationCapabilities - mocks GetMigrationCapabilities func -func (s *Replication) GetMigrationCapabilities(_ context2.Context, _ *migration.GetMigrationCapabilityRequest) (*migration.GetMigrationCapabilityResponse, error) { +func (s *Replication) GetMigrationCapabilities(_ context.Context, _ *migration.GetMigrationCapabilityRequest) (*migration.GetMigrationCapabilityResponse, error) { return &migration.GetMigrationCapabilityResponse{ Capabilities: []*migration.MigrationCapability{ {