Skip to content

Commit 395d870

Browse files
committed
Enhancement - Introduce pvcName input param in CNSUnregisterVolume spec
Adds validation rules using x-kubernetes-validations to ensure that only one of the volumeID or pvcName is specified to avoid ambiguity. Updates the reconciler to use the informer cache to find out the PV and VolumeID when the PVC Name is supplied. Optimises the reconciler logic to ignore all such events that do not increment the generation. Optimises the usage calculation logic by failing fast when usages are detected. Updates the reconciler to add a finalizer to the CR before reconciling to have control over deletion process Updates the reconciler to process the delete events and remove the finalizer for graceful deletion. Updates the reconciler to protect the PVC using a finalizer to gracefully reconciler in case of retries Removes the code that retains the persistent volume as it's no longer required Updates the reconciler to remove the finalizer on the PVC once unregistration is successful for graceful deletion of the PVC. Adds/updates unit tests wherever necessary and applicable.
1 parent b96d98d commit 395d870

File tree

14 files changed

+1196
-523
lines changed

14 files changed

+1196
-523
lines changed

pkg/apis/cnsoperator/cnsunregistervolume/v1alpha1/cnsunregistervolume_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import (
2424
// +k8s:openapi-gen=true
2525
type CnsUnregisterVolumeSpec struct {
2626
// VolumeID indicates the volume handle of CNS volume to be unregistered
27-
VolumeID string `json:"volumeID"`
27+
VolumeID string `json:"volumeID,omitempty"`
28+
29+
// PVCName indicates the name of the PVC to be unregistered.
30+
PVCName string `json:"pvcName,omitempty"`
2831

2932
// RetainFCD indicates if the volume should be retained as an FCD.
3033
// If set to false or not specified, the volume will be retained as a VMDK.
Lines changed: 62 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
---
32
apiVersion: apiextensions.k8s.io/v1
43
kind: CustomResourceDefinition
@@ -16,66 +15,73 @@ spec:
1615
singular: cnsunregistervolume
1716
scope: Namespaced
1817
versions:
19-
- name: v1alpha1
20-
schema:
21-
openAPIV3Schema:
22-
description: CnsUnregisterVolume is the Schema for the cnsunregistervolumes API
23-
properties:
24-
apiVersion:
25-
description: 'APIVersion defines the versioned schema of this representation
18+
- name: v1alpha1
19+
schema:
20+
openAPIV3Schema:
21+
description: CnsUnregisterVolume is the Schema for the cnsunregistervolumes API
22+
properties:
23+
apiVersion:
24+
description: 'APIVersion defines the versioned schema of this representation
2625
of an object. Servers should convert recognized schemas to the latest
2726
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
28-
type: string
29-
kind:
30-
description: 'Kind is a string value representing the REST resource this
27+
type: string
28+
kind:
29+
description: 'Kind is a string value representing the REST resource this
3130
object represents. Servers may infer this from the endpoint the client
3231
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
33-
type: string
34-
metadata:
35-
type: object
36-
spec:
37-
description: CnsUnregisterVolumeSpec defines the desired state of CnsUnregisterVolume
38-
properties:
39-
volumeID:
40-
description: VolumeID indicates the volume handle of CNS volume to be unregistered.
41-
type: string
42-
pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'
43-
retainFCD:
44-
description: RetainFCD indicates if the volume should be retained as an FCD.
45-
If set to false or not specified, the volume will be retained as a VMDK.
46-
type: boolean
47-
forceUnregister:
48-
description: ForceUnregister indicates if the volume should be forcefully unregistered.
49-
If set to true, the volume will be unregistered even if it is still in use by any VM.
50-
This should be used with caution as it may lead to data loss.
51-
type: boolean
52-
required:
53-
- volumeID
54-
type: object
55-
status:
56-
description: CnsUnregisterVolumeStatus defines the observed state of CnsUnregisterVolume
57-
properties:
58-
error:
59-
description: The last error encountered during export operation, if
60-
any. This field must only be set by the entity completing the export
61-
operation, i.e. the CNS Operator.
62-
type: string
63-
unregistered:
64-
description: Indicates the volume is successfully unregistered.
65-
This field must only be set by the entity completing the unregister
66-
operation, i.e. the CNS Operator.
67-
type: boolean
68-
required:
69-
- unregistered
70-
type: object
71-
type: object
72-
served: true
73-
storage: true
74-
subresources:
75-
status: {}
32+
type: string
33+
metadata:
34+
type: object
35+
spec:
36+
description: CnsUnregisterVolumeSpec defines the desired state of CnsUnregisterVolume
37+
properties:
38+
volumeID:
39+
description: VolumeID indicates the volume handle of CNS volume to be unregistered.
40+
type: string
41+
pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'
42+
pvcName:
43+
description: PVCName indicates the name of the PVC to be unregistered.
44+
type: string
45+
pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'
46+
retainFCD:
47+
description: RetainFCD indicates if the volume should be retained as an FCD.
48+
If set to false or not specified, the volume will be retained as a VMDK.
49+
type: boolean
50+
forceUnregister:
51+
description: ForceUnregister indicates if the volume should be forcefully unregistered.
52+
If set to true, the volume will be unregistered even if it is still in use by any VM.
53+
This should be used with caution as it may lead to data loss.
54+
type: boolean
55+
type: object
56+
x-kubernetes-validations:
57+
- rule: "has(self.volumeID) || has(self.pvcName)"
58+
message: "Either 'volumeID' or 'pvcName' must be specified, but not both"
59+
- rule: "!(has(self.volumeID) && has(self.pvcName))"
60+
message: "Cannot specify both 'volumeID' and 'pvcName' at the same time. Please specify only one of them"
61+
status:
62+
description: CnsUnregisterVolumeStatus defines the observed state of CnsUnregisterVolume
63+
properties:
64+
error:
65+
description: The last error encountered during export operation, if
66+
any. This field must only be set by the entity completing the export
67+
operation, i.e. the CNS Operator.
68+
type: string
69+
unregistered:
70+
description: Indicates the volume is successfully unregistered.
71+
This field must only be set by the entity completing the unregister
72+
operation, i.e. the CNS Operator.
73+
type: boolean
74+
required:
75+
- unregistered
76+
type: object
77+
type: object
78+
served: true
79+
storage: true
80+
subresources:
81+
status: { }
7682
status:
7783
acceptedNames:
7884
kind: ""
7985
plural: ""
80-
conditions: []
81-
storedVersions: []
86+
conditions: [ ]
87+
storedVersions: [ ]

pkg/common/cns-lib/volume/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3766,6 +3766,11 @@ func (m *defaultManager) UnregisterVolume(ctx context.Context, volumeID string,
37663766
func (m *defaultManager) unregisterVolume(ctx context.Context, volumeID string, unregisterDisk bool) error {
37673767
log := logger.GetLogger(ctx)
37683768

3769+
if volumeID == "" {
3770+
log.Debugf("UnregisterVolume: volumeID is empty, nothing to unregister")
3771+
return nil
3772+
}
3773+
37693774
if m.virtualCenter == nil {
37703775
return errors.New("invalid manager instance")
37713776
}

pkg/common/unittestcommon/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ func (c *FakeK8SOrchestrator) GetPVCNameFromCSIVolumeID(volumeID string) (string
431431
}
432432

433433
// GetVolumeIDFromPVCName simlates an invalid case when pvcName contains "invalid".
434-
func (c *FakeK8SOrchestrator) GetVolumeIDFromPVCName(pvcName string) (string, bool) {
434+
func (c *FakeK8SOrchestrator) GetVolumeIDFromPVCName(namespace string, pvcName string) (string, bool) {
435435
if strings.Contains(pvcName, "invalid") {
436436
// Simulate a case where the volumeID is invalid and does not correspond to any PVC.
437437
return "", false

pkg/csi/service/common/commonco/coagnostic.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ type COCommonInterface interface {
9898
GetPVNameFromCSIVolumeID(volumeID string) (string, bool)
9999
// GetPVCNameFromCSIVolumeID returns `pvc name` and `pvc namespace` for the given volumeID using volumeIDToPvcMap.
100100
GetPVCNameFromCSIVolumeID(volumeID string) (string, string, bool)
101-
// GetVolumeIDFromPVCName returns volumeID the given pvcName using pvcToVolumeIDMap.
102-
// PVC name is its namespaced name.
103-
GetVolumeIDFromPVCName(pvcName string) (string, bool)
101+
// GetVolumeIDFromPVCName returns volumeID for the given pvc name and namespace.
102+
GetVolumeIDFromPVCName(namespace string, pvcName string) (string, bool)
104103
// InitializeCSINodes creates CSINode instances for each K8s node with the appropriate topology keys.
105104
InitializeCSINodes(ctx context.Context) error
106105
// StartZonesInformer starts a dynamic informer which listens on Zones CR in

pkg/csi/service/common/commonco/k8sorchestrator/k8sorchestrator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2016,8 +2016,8 @@ func (c *K8sOrchestrator) GetPVCNameFromCSIVolumeID(volumeID string) (
20162016

20172017
// GetVolumeIDFromPVCName returns volumeID the given pvcName using pvcToVolumeIDMap.
20182018
// PVC name is its namespaced name.
2019-
func (c *K8sOrchestrator) GetVolumeIDFromPVCName(pvcName string) (string, bool) {
2020-
return c.pvcToVolumeIDMap.get(pvcName)
2019+
func (c *K8sOrchestrator) GetVolumeIDFromPVCName(namespace string, pvcName string) (string, bool) {
2020+
return c.pvcToVolumeIDMap.get(fmt.Sprintf("%s/%s", namespace, pvcName))
20212021
}
20222022

20232023
// IsLinkedCloneRequest checks if the pvc is a linked clone request

pkg/kubernetes/kubernetes.go

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import (
5858
snapshotterClientSet "github.com/kubernetes-csi/external-snapshotter/client/v8/clientset/versioned"
5959
storagev1 "k8s.io/api/storage/v1"
6060

61+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
6162
cnsoperatorv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator"
6263
migrationv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/migration/v1alpha1"
6364
storagepoolAPIs "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/storagepool"
@@ -712,100 +713,142 @@ func PatchFinalizers(ctx context.Context, c client.Client, obj client.Object, fi
712713
return c.Patch(ctx, obj, patch)
713714
}
714715

715-
// RetainPersistentVolume updates the PersistentVolume's ReclaimPolicy to Retain.
716-
// This is useful to preserve the PersistentVolume even if the associated PersistentVolumeClaim is deleted.
717-
func RetainPersistentVolume(ctx context.Context, k8sClient clientset.Interface, pvName string) error {
718-
log := logger.GetLogger(ctx)
716+
// DeletePersistentVolumeClaim deletes the PersistentVolumeClaim with the given name and namespace.
717+
func DeletePersistentVolumeClaim(ctx context.Context, k8sClient clientset.Interface,
718+
pvcName, pvcNamespace string) error {
719+
log := logger.GetLogger(ctx).With("name", fmt.Sprintf("%s/%s", pvcNamespace, pvcName))
719720

720-
if pvName == "" {
721-
log.Debugf("PersistentVolume name is empty. Exiting...")
721+
if pvcName == "" {
722+
log.Debug("PVC name is empty. Exiting...")
722723
return nil
723724
}
724725

725-
log.Debugf("Retaining PersistentVolume %q", pvName)
726-
pv, err := k8sClient.CoreV1().PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{})
726+
log.Debug("Deleting PVC")
727+
err := k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Delete(ctx, pvcName, metav1.DeleteOptions{})
727728
if err != nil {
728729
if apierrors.IsNotFound(err) {
729-
log.Debugf("PersistentVolume %q not found. Exiting...", pvName)
730+
log.Debug("PVC not found. Exiting...")
730731
return nil
731732
}
732733

733-
return logger.LogNewErrorf(log, "Failed to get PersistentVolume %q. Error: %s", pvName, err.Error())
734+
return logger.LogNewErrorf(log, "Failed to delete PVC. Error: %s", err.Error())
735+
}
736+
737+
log.Debug("Successfully deleted PVC")
738+
return nil
739+
}
740+
741+
// DeletePersistentVolume deletes the PersistentVolume with the given name.
742+
func DeletePersistentVolume(ctx context.Context, k8sClient clientset.Interface, pvName string) error {
743+
log := logger.GetLogger(ctx).With("name", pvName)
744+
745+
if pvName == "" {
746+
log.Debug("PersistentVolume name is empty. Exiting...")
747+
return nil
734748
}
735749

736-
pv.Spec.PersistentVolumeReclaimPolicy = v1.PersistentVolumeReclaimRetain
737-
_, err = k8sClient.CoreV1().PersistentVolumes().Update(ctx, pv, metav1.UpdateOptions{})
750+
log.Debug("Deleting PV")
751+
err := k8sClient.CoreV1().PersistentVolumes().Delete(ctx, pvName, metav1.DeleteOptions{})
738752
if err != nil {
739-
return logger.LogNewErrorf(log, "Failed to update PersistentVolume %q to retain policy. Error: %s",
740-
pvName, err.Error())
753+
if apierrors.IsNotFound(err) {
754+
log.Debug("PV not found. Exiting...")
755+
return nil
756+
}
757+
758+
return logger.LogNewErrorf(log, "Failed to delete PV. Error: %s", err.Error())
741759
}
742760

743-
log.Debugf("Successfully retained PersistentVolume %q", pvName)
761+
log.Debug("Successfully deleted PV")
744762
return nil
745763
}
746764

747-
// DeletePersistentVolumeClaim deletes the PersistentVolumeClaim with the given name and namespace.
748-
func DeletePersistentVolumeClaim(ctx context.Context, k8sClient clientset.Interface,
749-
pvcName, pvcNamespace string) error {
750-
log := logger.GetLogger(ctx)
765+
// UpdateStatus updates the status subresource of the given Kubernetes object.
766+
// If the object is a Custom Resource, make sure that the `subresources` field in the
767+
// CustomResourceDefinition includes `status` to enable status subresource updates.
768+
func UpdateStatus(ctx context.Context, c client.Client, obj client.Object) error {
769+
log := logger.GetLogger(ctx).With(
770+
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
771+
"name", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()),
772+
)
773+
if err := c.Status().Update(ctx, obj); err != nil {
774+
log.Errorf("Failed to update status. Error: %s", err)
775+
return err
776+
}
777+
778+
log.Info("Successfully updated status")
779+
return nil
780+
}
781+
782+
// AddFinalizerOnPVC adds the specified finalizer to the PersistentVolumeClaim (PVC)
783+
// if it is not already present.
784+
func AddFinalizerOnPVC(ctx context.Context, k8sClient clientset.Interface, pvcName, pvcNamespace,
785+
finalizer string) error {
786+
log := logger.GetLogger(ctx).With("name", fmt.Sprintf("%s/%s", pvcNamespace, pvcName))
751787

752788
if pvcName == "" {
753-
log.Debugf("PVC name is empty. Exiting...")
789+
log.Debug("PVC name is empty. Exiting...")
754790
return nil
755791
}
756792

757-
log.Debugf("Deleting PersistentVolumeClaim %q in namespace %q", pvcName, pvcNamespace)
758-
err := k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Delete(ctx, pvcName, metav1.DeleteOptions{})
793+
pvc, err := k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Get(ctx, pvcName, metav1.GetOptions{})
759794
if err != nil {
760795
if apierrors.IsNotFound(err) {
761-
log.Debugf("PersistentVolumeClaim %q in namespace %q not found. Exiting...", pvcName, pvcNamespace)
796+
log.Debug("PVC not found. Exiting...", pvcName, pvcNamespace)
762797
return nil
763798
}
764799

765-
return logger.LogNewErrorf(log, "Failed to delete PersistentVolumeClaim %q in namespace %q. Error: %s",
766-
pvcName, pvcNamespace, err.Error())
800+
return logger.LogNewErrorf(log, "Failed to get PVC. Error: %s", err.Error())
801+
}
802+
803+
// If the finalizer is already present, no action is needed
804+
if !controllerutil.AddFinalizer(pvc, finalizer) {
805+
log.Debugf("Finalizer %s already present on PVC. No action needed.", finalizer)
806+
return nil
807+
}
808+
809+
// Update the PVC with the new finalizer
810+
_, err = k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Update(ctx, pvc, metav1.UpdateOptions{})
811+
if err != nil {
812+
return logger.LogNewErrorf(log, "Failed to add finalizer %s to PVC. Error: %s", finalizer, err.Error())
767813
}
768814

769-
log.Debugf("Successfully deleted PersistentVolumeClaim %q in namespace %q", pvcName, pvcNamespace)
815+
log.Debugf("Successfully added finalizer %s to PVC", finalizer)
770816
return nil
771817
}
772818

773-
// DeletePersistentVolume deletes the PersistentVolume with the given name.
774-
func DeletePersistentVolume(ctx context.Context, k8sClient clientset.Interface, pvName string) error {
775-
log := logger.GetLogger(ctx)
819+
// RemoveFinalizerFromPVC removes the specified finalizer from the PersistentVolumeClaim (PVC)
820+
// if it is present.
821+
func RemoveFinalizerFromPVC(ctx context.Context, k8sClient clientset.Interface, pvcName, pvcNamespace,
822+
finalizer string) error {
823+
log := logger.GetLogger(ctx).With("name", fmt.Sprintf("%s/%s", pvcNamespace, pvcName))
776824

777-
if pvName == "" {
778-
log.Debugf("PersistentVolume name is empty. Exiting...")
825+
if pvcName == "" {
826+
log.Debug("PVC name is empty. Exiting...")
779827
return nil
780828
}
781829

782-
log.Debugf("Deleting PersistentVolume %q", pvName)
783-
err := k8sClient.CoreV1().PersistentVolumes().Delete(ctx, pvName, metav1.DeleteOptions{})
830+
pvc, err := k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Get(ctx, pvcName, metav1.GetOptions{})
784831
if err != nil {
785832
if apierrors.IsNotFound(err) {
786-
log.Debugf("PersistentVolume %q not found. Exiting...", pvName)
833+
log.Debug("PVC not found. Exiting...", pvcName, pvcNamespace)
787834
return nil
788835
}
789836

790-
return logger.LogNewErrorf(log, "Failed to delete PersistentVolume %q. Error: %s", pvName, err.Error())
837+
return logger.LogNewErrorf(log, "Failed to get PVC. Error: %s", err.Error())
791838
}
792839

793-
log.Debugf("Successfully deleted PersistentVolume %q", pvName)
794-
return nil
795-
}
840+
// If the finalizer is not present, no action is needed
841+
if !controllerutil.RemoveFinalizer(pvc, finalizer) {
842+
log.Debugf("Finalizer %s not present on PVC. No action needed.", finalizer)
843+
return nil
844+
}
796845

797-
// UpdateStatus updates the status subresource of the given Kubernetes object.
798-
// If the object is a Custom Resource, make sure that the `subresources` field in the
799-
// CustomResourceDefinition includes `status` to enable status subresource updates.
800-
func UpdateStatus(ctx context.Context, c client.Client, obj client.Object) error {
801-
log := logger.GetLogger(ctx)
802-
if err := c.Status().Update(ctx, obj); err != nil {
803-
log.Errorf("Failed to update status for %s %s/%s: %v", obj.GetObjectKind().GroupVersionKind().Kind,
804-
obj.GetNamespace(), obj.GetName(), err)
805-
return err
846+
// Update the PVC to remove the finalizer
847+
_, err = k8sClient.CoreV1().PersistentVolumeClaims(pvcNamespace).Update(ctx, pvc, metav1.UpdateOptions{})
848+
if err != nil {
849+
return logger.LogNewErrorf(log, "Failed to remove finalizer %s from PVC. Error: %s", finalizer, err.Error())
806850
}
807851

808-
log.Infof("Successfully updated status for %s %s/%s", obj.GetObjectKind().GroupVersionKind().Kind,
809-
obj.GetNamespace(), obj.GetName())
852+
log.Debugf("Successfully removed finalizer %s from PVC", finalizer)
810853
return nil
811854
}

pkg/syncer/admissionhandler/cnscsi_admissionhandler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func (m *MockCOCommonInterface) GetPVCNameFromCSIVolumeID(volumeID string) (stri
188188
return args.String(0), args.String(1), args.Bool(2)
189189
}
190190

191-
func (m *MockCOCommonInterface) GetVolumeIDFromPVCName(pvcName string) (string, bool) {
191+
func (m *MockCOCommonInterface) GetVolumeIDFromPVCName(namespace, pvcName string) (string, bool) {
192192
args := m.Called(pvcName)
193193
return args.String(0), args.Bool(1)
194194
}

0 commit comments

Comments
 (0)