Skip to content

Commit 0872e3c

Browse files
committed
[VMOwnedVolumes][Draft]Core Workflows — Attach, Snapshot, Detach, Snapshot Deletion, Revert Re-adoption
Signed-off-by: Deepak Kinni <deepak.kinni@broadcom.com>
1 parent 36b5977 commit 0872e3c

12 files changed

Lines changed: 1912 additions & 17 deletions

File tree

pkg/apis/cnsoperator/cnsnodevmbatchattachment/v1alpha1/cnsnodebatchvmattachment_types.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,20 @@ const (
6161
// ReasonDetachBlocked reflects that a ReconfigVM remove was blocked, typically
6262
// because a vSphere snapshot still retains the disk.
6363
ReasonDetachBlocked = "DetachBlocked"
64+
65+
// ConditionAttachMethod is the condition type set by CSI on a per-volume basis
66+
// to communicate to vm-operator which attach mechanism was used.
67+
// "Reconfig" means CSI unregistered the FCD and vm-operator should use
68+
// ReconfigVM_Task with a file-backed disk. "CnsAttach" means CSI used the
69+
// legacy CNS attach path.
70+
ConditionAttachMethod = "AttachMethod"
71+
// ReasonReconfig is the condition reason for ConditionAttachMethod when CSI
72+
// has unregistered the FCD and placed diskPath + diskUUID on the volume
73+
// status for vm-operator to use in ReconfigVM_Task.
74+
ReasonReconfig = "Reconfig"
75+
// ReasonCnsAttach is the condition reason for ConditionAttachMethod when CSI
76+
// followed the legacy BatchAttachVolumes path (brownfield or FSS disabled).
77+
ReasonCnsAttach = "CnsAttach"
6478
)
6579

6680
// SharingMode is the sharing mode of the virtual disk.
@@ -152,10 +166,17 @@ type PersistentVolumeClaimStatus struct {
152166
CnsVolumeID string `json:"cnsVolumeId,omitempty"`
153167
// +optional
154168

155-
// DiskUUID is the ID obtained when volume is attached to a VM.
169+
// DiskUUID is the stable identifier for the virtual disk
170+
// (VirtualDisk.Backing.Uuid). Populated by CSI on greenfield attach so
171+
// that vm-operator can verify the disk matches after ReconfigVM_Task.
156172
DiskUUID string `json:"diskUUID,omitempty"`
157173
// +optional
158174

175+
// DiskPath is the datastore path for the VMDK (populated by CSI on
176+
// greenfield attach for vm-operator to use in ReconfigVM_Task).
177+
DiskPath string `json:"diskPath,omitempty"`
178+
// +optional
179+
159180
// Conditions describes any conditions associated with this volume.
160181
Conditions []metav1.Condition `json:"conditions,omitempty"`
161182
}

pkg/apis/cnsoperator/csivolumeinfo/csivolumeinfoservice.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/apimachinery/pkg/labels"
3030
k8stypes "k8s.io/apimachinery/pkg/types"
3131
"sigs.k8s.io/controller-runtime/pkg/client"
32+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3233

3334
csivolumeinfov1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/csivolumeinfo/v1alpha1"
3435
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/logger"
@@ -86,6 +87,16 @@ type CsiVolumeInfoService interface {
8687
// error when no matching CR is found.
8788
GetCsiVolumeInfoByPVCName(ctx context.Context, namespace, pvcName string) (
8889
*csivolumeinfov1alpha1.CsiVolumeInfo, error)
90+
91+
// AddCVIProtectionFinalizer adds the cvi-protection finalizer to the
92+
// CsiVolumeInfo for the given volumeID in namespace. It is idempotent:
93+
// if the finalizer is already present, the call is a no-op.
94+
AddCVIProtectionFinalizer(ctx context.Context, namespace, volumeID string) error
95+
96+
// RemoveCVIProtectionFinalizer removes the cvi-protection finalizer from the
97+
// CsiVolumeInfo for the given volumeID in namespace. It is idempotent:
98+
// if the finalizer is absent, the call is a no-op.
99+
RemoveCVIProtectionFinalizer(ctx context.Context, namespace, volumeID string) error
89100
}
90101

91102
// csiVolumeInfoSvc is the concrete singleton implementing CsiVolumeInfoService.
@@ -337,6 +348,72 @@ func (s *csiVolumeInfoSvc) CsiVolumeInfoExists(
337348
return cvi != nil, nil
338349
}
339350

351+
// AddCVIProtectionFinalizer adds the cvi-protection finalizer to the CsiVolumeInfo
352+
// identified by volumeID / namespace. The operation is idempotent.
353+
func (s *csiVolumeInfoSvc) AddCVIProtectionFinalizer(
354+
ctx context.Context, namespace, volumeID string) error {
355+
log := logger.GetLogger(ctx)
356+
name := GetCsiVolumeInfoCRName(volumeID)
357+
358+
cvi, err := s.GetCsiVolumeInfo(ctx, namespace, volumeID)
359+
if err != nil {
360+
return fmt.Errorf("AddCVIProtectionFinalizer: failed to fetch CsiVolumeInfo %s/%s: %w",
361+
namespace, name, err)
362+
}
363+
if cvi == nil {
364+
return fmt.Errorf("AddCVIProtectionFinalizer: CsiVolumeInfo %s/%s not found",
365+
namespace, name)
366+
}
367+
if controllerutil.ContainsFinalizer(cvi, csivolumeinfov1alpha1.CVIProtectionFinalizer) {
368+
log.Infof("AddCVIProtectionFinalizer: finalizer already present on CsiVolumeInfo %s/%s",
369+
namespace, name)
370+
return nil
371+
}
372+
373+
patch := client.MergeFrom(cvi.DeepCopy())
374+
controllerutil.AddFinalizer(cvi, csivolumeinfov1alpha1.CVIProtectionFinalizer)
375+
if err := s.k8sClient.Patch(ctx, cvi, patch); err != nil {
376+
return fmt.Errorf("AddCVIProtectionFinalizer: failed to patch CsiVolumeInfo %s/%s: %w",
377+
namespace, name, err)
378+
}
379+
log.Infof("AddCVIProtectionFinalizer: added finalizer on CsiVolumeInfo %s/%s", namespace, name)
380+
return nil
381+
}
382+
383+
// RemoveCVIProtectionFinalizer removes the cvi-protection finalizer from the
384+
// CsiVolumeInfo identified by volumeID / namespace. The operation is idempotent.
385+
func (s *csiVolumeInfoSvc) RemoveCVIProtectionFinalizer(
386+
ctx context.Context, namespace, volumeID string) error {
387+
log := logger.GetLogger(ctx)
388+
name := GetCsiVolumeInfoCRName(volumeID)
389+
390+
cvi, err := s.GetCsiVolumeInfo(ctx, namespace, volumeID)
391+
if err != nil {
392+
return fmt.Errorf("RemoveCVIProtectionFinalizer: failed to fetch CsiVolumeInfo %s/%s: %w",
393+
namespace, name, err)
394+
}
395+
if cvi == nil {
396+
log.Infof("RemoveCVIProtectionFinalizer: CsiVolumeInfo %s/%s not found; no-op",
397+
namespace, name)
398+
return nil
399+
}
400+
if !controllerutil.ContainsFinalizer(cvi, csivolumeinfov1alpha1.CVIProtectionFinalizer) {
401+
log.Infof("RemoveCVIProtectionFinalizer: finalizer absent on CsiVolumeInfo %s/%s; no-op",
402+
namespace, name)
403+
return nil
404+
}
405+
406+
patch := client.MergeFrom(cvi.DeepCopy())
407+
controllerutil.RemoveFinalizer(cvi, csivolumeinfov1alpha1.CVIProtectionFinalizer)
408+
if err := s.k8sClient.Patch(ctx, cvi, patch); err != nil {
409+
return fmt.Errorf("RemoveCVIProtectionFinalizer: failed to patch CsiVolumeInfo %s/%s: %w",
410+
namespace, name, err)
411+
}
412+
log.Infof("RemoveCVIProtectionFinalizer: removed finalizer from CsiVolumeInfo %s/%s",
413+
namespace, name)
414+
return nil
415+
}
416+
340417
// GetCsiVolumeInfoByPVCName lists all CsiVolumeInfo CRs in the given namespace
341418
// and returns the one whose spec.pvcName matches pvcName. Returns nil and no
342419
// error when no matching CR is found. If multiple CRs match (data integrity

pkg/syncer/admissionhandler/validatepvc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func validatePVC(ctx context.Context, req *admissionv1.AdmissionRequest) *admiss
133133

134134
// Block deletion of snapshot-retained PVCs tracked by CsiVolumeInfo.
135135
if req.Operation == admissionv1.Delete &&
136+
commonco.ContainerOrchestratorUtility != nil &&
136137
commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.VMOwnedVolumes) {
137138
if cviDenied, cviMsg := checkCVISnapshotRetained(ctx, oldPVC.Namespace, oldPVC.Name); cviDenied {
138139
return &admissionv1.AdmissionResponse{

pkg/syncer/cnsoperator/controller/cnsnodevmbatchattachment/cnsnodevmbatchattachment_controller.go

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import (
5555
cbtconfigv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cbtconfig/v1alpha1"
5656
cnsoperatorapis "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator"
5757
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/cnsnodevmbatchattachment/v1alpha1"
58+
csivolumeinfo "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/csivolumeinfo"
5859
volumes "sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/cns-lib/volume"
5960
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/common/config"
6061
"sigs.k8s.io/vsphere-csi-driver/v3/pkg/csi/service/common"
@@ -165,21 +166,38 @@ func Add(mgr manager.Manager, clusterFlavor cnstypes.CnsClusterFlavor,
165166
}
166167

167168
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: cnsoperatorapis.GroupName})
169+
170+
// Initialise the CsiVolumeInfo service when the VM-owned volumes FSS is
171+
// enabled. The service is a package-level singleton; errors are non-fatal
172+
// here — the reconciler will fall back to the legacy attach path.
173+
var cviSvc csivolumeinfo.CsiVolumeInfoService
174+
if commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.VMOwnedVolumes) {
175+
var cviErr error
176+
cviSvc, cviErr = csivolumeinfo.InitCsiVolumeInfoService(ctx)
177+
if cviErr != nil {
178+
log.Warnf("CsiVolumeInfo service initialisation failed; VMOwnedVolumes attach path will be skipped. Err: %v",
179+
cviErr)
180+
cviSvc = nil
181+
}
182+
}
183+
168184
return add(mgr, newReconciler(mgr, configInfo, volumeManager, vmOperatorClient, cnsOperatorClient,
169-
recorder, cbtClient))
185+
recorder, cbtClient, cviSvc))
170186
}
171187

172188
func newReconciler(mgr manager.Manager, configInfo *config.ConfigurationInfo,
173189
volumeManager volumes.Manager, vmOperatorClient client.Client,
174190
cnsOperatorClient client.Client,
175-
recorder record.EventRecorder, cbtClient client.Client) reconcile.Reconciler {
191+
recorder record.EventRecorder, cbtClient client.Client,
192+
cviSvc csivolumeinfo.CsiVolumeInfoService) reconcile.Reconciler {
176193
return &Reconciler{client: mgr.GetClient(),
177194
scheme: mgr.GetScheme(),
178195
configInfo: *configInfo, volumeManager: volumeManager,
179196
vmOperatorClient: vmOperatorClient,
180197
cnsOperatorClient: cnsOperatorClient,
181198
recorder: recorder,
182199
cbtClient: cbtClient,
200+
cviService: cviSvc,
183201
instanceLock: sync.Map{}}
184202
}
185203

@@ -222,6 +240,10 @@ type Reconciler struct {
222240
// cbtClient is set when isCSIBackupAPIEnabled (CSI_Backup_API FSS on); used for
223241
// namespace-scoped CBTConfig lookups before batch attach.
224242
cbtClient client.Client
243+
// cviService provides CRUD operations for CsiVolumeInfo CRs.
244+
// It is set when the VMOwnedVolumes FSS is enabled and is used by the
245+
// ownership-transfer attach and detach paths.
246+
cviService csivolumeinfo.CsiVolumeInfoService
225247
// instanceLock to ensure that for an instance we have only
226248
// one reconciliation at a time.
227249
instanceLock sync.Map
@@ -470,10 +492,52 @@ func (r *Reconciler) detachVolumes(ctx context.Context,
470492

471493
volumesThatFailedToDetach := make([]string, 0)
472494

495+
// When the VMOwnedVolumes FSS is enabled, resolve the VM name once and
496+
// check whether the VM uses the ownership-transfer detach path.
497+
var vmName string
498+
var isVMOwnedVolumesVM bool
499+
if commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.VMOwnedVolumes) &&
500+
r.cviService != nil {
501+
var vmNameErr error
502+
vmName, vmNameErr = getVMNameByInstanceUUID(ctx, r.vmOperatorClient,
503+
instance.Spec.InstanceUUID, instance.Namespace)
504+
if vmNameErr != nil {
505+
log.Warnf("detachVolumes: failed to resolve VM name for instanceUUID %q; "+
506+
"using legacy detach path for all volumes. Err: %v",
507+
instance.Spec.InstanceUUID, vmNameErr)
508+
} else if vmName != "" {
509+
var checkErr error
510+
isVMOwnedVolumesVM, checkErr = IsVMOwnedVolumesVM(ctx, r.vmOperatorClient,
511+
instance.Namespace, vmName)
512+
if checkErr != nil {
513+
log.Warnf("detachVolumes: IsVMOwnedVolumesVM check failed for %s/%s; "+
514+
"falling back to legacy detach. Err: %v",
515+
instance.Namespace, vmName, checkErr)
516+
isVMOwnedVolumesVM = false
517+
}
518+
}
519+
}
520+
473521
for pvc, volumeId := range volumesToDetach {
474522
log.Infof("Detach call started for PVC %s with volumeID %s in namespace %s for instance %s",
475523
pvc, volumeId, instance.Namespace, instance.Name)
476524

525+
// For VMs with the VMOwnedVolumes annotation all volumes use the
526+
// ownership-transfer detach path — the legacy CNS DetachVolume is
527+
// never called for these VMs.
528+
if isVMOwnedVolumesVM {
529+
if detachErr := reconcileVMOwnedVolumesDetach(ctx,
530+
r.cviService, r.volumeManager, r.client,
531+
vm, instance, pvc, volumeId, &r.configInfo); detachErr != nil {
532+
log.Errorf("detachVolumes: VMOwnedVolumes detach failed for PVC %s: %v",
533+
pvc, detachErr)
534+
updateInstanceVolumeStatus(ctx, instance, "", pvc, "", "", detachErr,
535+
v1alpha1.ConditionDetached, v1alpha1.ReasonDetachFailed)
536+
volumesThatFailedToDetach = append(volumesThatFailedToDetach, pvc)
537+
}
538+
continue
539+
}
540+
477541
// Call CNS DetachVolume
478542
faulttype, detachErr := r.volumeManager.DetachVolume(ctx, vm, volumeId)
479543
if detachErr != nil {
@@ -496,7 +560,7 @@ func (r *Reconciler) detachVolumes(ctx context.Context,
496560
// Remove finalizer from the PVC as the detach was successful.
497561
volumesThatFailedToDetach = removeFinalizerAndStatusEntry(ctx, r.client, k8sClient, r.cnsOperatorClient,
498562
instance, pvc, volumesThatFailedToDetach)
499-
// Attempt lazy CVI creation for brownfield volumes when preconditions are met.
563+
// Attempt lazy CVI creation for volumes provisioned before the VMOwnedVolumes FSS was enabled.
500564
go MaybeLazilyCreateCVI(ctx, vm, r.volumeManager,
501565
instance.Namespace,
502566
volumeId, pvc, "", "",
@@ -573,12 +637,62 @@ func (r *Reconciler) processBatchAttach(ctx context.Context, k8sClient kubernete
573637
instance.Namespace)
574638
}
575639

640+
// When the VMOwnedVolumes FSS is enabled, check once whether this VM
641+
// carries the annotation. If it does, all volumes use the
642+
// ownership-transfer attach path and BatchAttachVolumes is not called.
643+
if commonco.ContainerOrchestratorUtility.IsFSSEnabled(ctx, common.VMOwnedVolumes) &&
644+
r.cviService != nil {
645+
vmName, vmNameErr := getVMNameByInstanceUUID(ctx, r.vmOperatorClient,
646+
instance.Spec.InstanceUUID, instance.Namespace)
647+
if vmNameErr != nil {
648+
log.Warnf("processBatchAttach: failed to resolve VM name for instanceUUID %q; "+
649+
"falling back to legacy attach path. Err: %v",
650+
instance.Spec.InstanceUUID, vmNameErr)
651+
} else if vmName != "" {
652+
isVMOwned, checkErr := IsVMOwnedVolumesVM(ctx, r.vmOperatorClient,
653+
instance.Namespace, vmName)
654+
if checkErr != nil {
655+
log.Warnf("processBatchAttach: IsVMOwnedVolumesVM check failed for %s/%s; "+
656+
"falling back to legacy attach path. Err: %v",
657+
instance.Namespace, vmName, checkErr)
658+
} else if isVMOwned {
659+
log.Infof("processBatchAttach: VM %s/%s uses ownership-transfer attach path",
660+
instance.Namespace, vmName)
661+
for pvcName, volumeID := range volumesToAttach {
662+
// Derive volumeName for status updates from the spec.
663+
var volumeName string
664+
for _, v := range instance.Spec.Volumes {
665+
if v.PersistentVolumeClaim.ClaimName == pvcName {
666+
volumeName = v.Name
667+
break
668+
}
669+
}
670+
if attachErr := processVMOwnedVolumesAttach(ctx,
671+
r.cviService, r.vmOperatorClient, r.volumeManager,
672+
instance, vmName, instance.Spec.InstanceUUID,
673+
pvcName, volumeID, volumeName); attachErr != nil {
674+
log.Errorf("processBatchAttach: VMOwnedVolumes attach failed for PVC %s: %v",
675+
pvcName, attachErr)
676+
err = errors.Join(err, attachErr)
677+
}
678+
}
679+
// Persist BA status changes and return — this path never
680+
// falls through to BatchAttachVolumes.
681+
if patchErr := patchBAStatus(ctx, r.client, instance); patchErr != nil {
682+
log.Errorf("processBatchAttach: failed to patch BA status: %v", patchErr)
683+
err = errors.Join(err, patchErr)
684+
}
685+
return err
686+
}
687+
}
688+
}
689+
576690
// Construct batch attach request
577-
pvcsInAttachList, volumeIdsInAttachList, batchAttachRequest, err := constructBatchAttachRequest(ctx,
691+
pvcsInAttachList, volumeIdsInAttachList, batchAttachRequest, batchErr := constructBatchAttachRequest(ctx,
578692
volumesToAttach, instance)
579-
if err != nil {
580-
log.Errorf("failed to construct batch attach request. Err: %s", err)
581-
return err
693+
if batchErr != nil {
694+
log.Errorf("failed to construct batch attach request. Err: %s", batchErr)
695+
return errors.Join(err, batchErr)
582696
}
583697

584698
// Manage CBT for each volume before batch attach.
@@ -619,10 +733,11 @@ func (r *Reconciler) processBatchAttach(ctx context.Context, k8sClient kubernete
619733
if result.Error == nil {
620734
reason = ""
621735
// Add finalizer on PVC as attach was successful.
622-
err = addPvcFinalizer(ctx, r.client, k8sClient, pvcName, instance.Namespace, instance.Spec.InstanceUUID)
623-
if err != nil {
736+
finalizerErr := addPvcFinalizer(ctx, r.client, k8sClient, pvcName,
737+
instance.Namespace, instance.Spec.InstanceUUID)
738+
if finalizerErr != nil {
624739
log.Errorf("failed to add finalizer %s on PVC %s", cnsoperatortypes.CNSPvcFinalizer, pvcName)
625-
result.Error = err
740+
result.Error = finalizerErr
626741
attachErr = errors.Join(attachErr,
627742
fmt.Errorf("failure during attach of PVC %s", pvcName))
628743
}
@@ -632,7 +747,7 @@ func (r *Reconciler) processBatchAttach(ctx context.Context, k8sClient kubernete
632747
v1alpha1.ConditionAttached, reason)
633748

634749
}
635-
return attachErr
750+
return errors.Join(err, attachErr)
636751
}
637752

638753
// recordEvent records the event, sets the backOffDuration for the instance

0 commit comments

Comments
 (0)