diff --git a/cmd/transfer-pvc/indirect.go b/cmd/transfer-pvc/indirect.go index 4569408e..c5e7fe10 100644 --- a/cmd/transfer-pvc/indirect.go +++ b/cmd/transfer-pvc/indirect.go @@ -139,8 +139,8 @@ func (t *TransferPVCCommand) runIndirect() error { // Create destination PVC fmt.Fprintf(os.Stderr, "[2/6] Creating destination PVC ...\n") destPVC := t.buildDestinationPVC(srcPVC) - err = destClient.Create(context.TODO(), destPVC, &client.CreateOptions{}) - if err != nil && !errors.IsAlreadyExists(err) { + err = t.createDestinationPVC(context.TODO(), destClient, destPVC) + if err != nil { log.Debugf("Unable to create destination PVC %s/%s: %v", destPVC.Namespace, destPVC.Name, err) return fmt.Errorf("unable to create destination PVC: %w", err) } diff --git a/cmd/transfer-pvc/transfer-pvc.go b/cmd/transfer-pvc/transfer-pvc.go index 0fba7a8e..1f65f4fb 100644 --- a/cmd/transfer-pvc/transfer-pvc.go +++ b/cmd/transfer-pvc/transfer-pvc.go @@ -421,8 +421,8 @@ func (t *TransferPVCCommand) run() (retErr error) { log.Infof("Phase 2: Creating destination PVC") phases.Start("Creating destination PVC") destPVC := t.buildDestinationPVC(srcPVC) - err = destClient.Create(context.TODO(), destPVC, &client.CreateOptions{}) - if err != nil && !errors.IsAlreadyExists(err) { + err = t.createDestinationPVC(context.TODO(), destClient, destPVC) + if err != nil { log.Errorf("Unable to create destination PVC %s/%s: %v", t.PVC.Namespace.destination, t.PVC.Name.destination, err) return phases.Fail(err, "unable to create destination PVC") } @@ -1290,6 +1290,35 @@ func (t *TransferPVCCommand) buildDestinationPVC(sourcePVC *corev1.PersistentVol return pvc } +// createDestinationPVC creates the destination PVC, validating the storage +// class of an existing PVC when --dest-storage-class was requested. +func (t *TransferPVCCommand) createDestinationPVC(ctx context.Context, c client.Client, pvc *corev1.PersistentVolumeClaim) error { + if err := c.Create(ctx, pvc, &client.CreateOptions{}); err == nil { + return nil + } else if !errors.IsAlreadyExists(err) { + return fmt.Errorf("creating destination PVC %q: %w", pvc.Name, err) + } + + if t.PVC.StorageClassName == "" { + return nil + } + + existing := &corev1.PersistentVolumeClaim{} + if err := c.Get(ctx, client.ObjectKeyFromObject(pvc), existing); err != nil { + return fmt.Errorf("getting existing destination PVC %q: %w", pvc.Name, err) + } + + existingStorageClass := "" + if existing.Spec.StorageClassName != nil { + existingStorageClass = *existing.Spec.StorageClassName + } + if existingStorageClass != t.PVC.StorageClassName { + return fmt.Errorf("destination PVC %q already exists with StorageClass %q but --dest-storage-class %q was requested; delete the existing PVC or omit --dest-storage-class to use the existing PVC as-is", pvc.Name, existingStorageClass, t.PVC.StorageClassName) + } + + return nil +} + func stripServerManagedPVCAnnotations(annotations map[string]string) map[string]string { if len(annotations) == 0 { return nil diff --git a/cmd/transfer-pvc/transfer-pvc_test.go b/cmd/transfer-pvc/transfer-pvc_test.go index cb032476..490d2a33 100644 --- a/cmd/transfer-pvc/transfer-pvc_test.go +++ b/cmd/transfer-pvc/transfer-pvc_test.go @@ -64,6 +64,53 @@ func newTestScheme() *runtime.Scheme { return s } +func TestCreateDestinationPVC(t *testing.T) { + storageClass := func(name string) *string { return &name } + tests := []struct { + name string + requestedStorageClass string + existingStorageClass *string + wantErr string + }{ + { + name: "rejects existing PVC with a different requested storage class", + requestedStorageClass: "standard-v2", + existingStorageClass: storageClass("crane-sc02-target"), + wantErr: `destination PVC "test-pvc" already exists with StorageClass "crane-sc02-target" but --dest-storage-class "standard-v2" was requested`, + }, + { + name: "accepts existing PVC with requested storage class", + requestedStorageClass: "standard-v2", + existingStorageClass: storageClass("standard-v2"), + }, + { + name: "accepts existing PVC when no storage class was requested", + existingStorageClass: storageClass("crane-sc02-target"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + existing := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "test-pvc", Namespace: "test-ns"}, + Spec: corev1.PersistentVolumeClaimSpec{StorageClassName: tt.existingStorageClass}, + } + c := fake.NewClientBuilder().WithScheme(newTestScheme()).WithObjects(existing).Build() + cmd := &TransferPVCCommand{Flags: Flags{PVC: PvcFlags{StorageClassName: tt.requestedStorageClass}}} + + err := cmd.createDestinationPVC(context.Background(), c, &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "test-pvc", Namespace: "test-ns"}, + }) + if tt.wantErr == "" && err != nil { + t.Fatalf("createDestinationPVC() unexpected error = %v", err) + } + if tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)) { + t.Errorf("createDestinationPVC() error = %v, want to contain %q", err, tt.wantErr) + } + }) + } +} + func Test_parseSourceDestinationMapping(t *testing.T) { tests := []struct { name string