Repository: kubernetes-csi/external-snapshotter
Pull request #1069 (2024-04-18 11:46:05)

Link PVCs and PVs in VolumeGroupSnapshot and VolumeGroupSnapshotContent
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Follow the instructions for writing a release note: https://git.k8s.io/community/contributors/guide/release-notes.md
3. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

**What type of PR is this?**
> Uncomment only one ` /kind <>` line, hit enter to put that in a new line, and remove leading whitespaces from that line:
>
> /kind api-change
> /kind bug
> /kind cleanup
> /kind design
> /kind documentation
> /kind failing-test
/kind feature
> /kind flake

**What this PR does / why we need it**:

Following #1068 and the discussion in #969, this PR improves the snapshot controller and the CSI snapshotter sidecar to track which PVC and PVs are snapshotted by a VolumeGroupSnapshot.

With this PR applied, a `VolumeGroupSnapshot` will look like:

```
apiVersion: groupsnapshot.storage.k8s.io/v1alpha1
kind: VolumeGroupSnapshot
metadata:
  finalizers:
  - groupsnapshot.storage.kubernetes.io/volumegroupsnapshot-bound-protection
  generation: 1
  name: new-groupsnapshot
  namespace: default
  resourceVersion: "64488"
  uid: 02f28293-ff7e-4d1c-8ea8-c64fb2b45a26
spec:
  source:
    selector:
      matchLabels:
        cnpg.io/instanceName: cluster-example-1
  volumeGroupSnapshotClassName: csi-hostpath-groupsnapclass
status:
  boundVolumeGroupSnapshotContentName: groupsnapcontent-02f28293-ff7e-4d1c-8ea8-c64fb2b45a26
  creationTime: "2024-05-14T10:02:50Z"
  pvcVolumeSnapshotRefList:
  - persistentVolumeClaimRef:
      name: cluster-example-1
    volumeSnapshotRef:
      name: snapshot-219cbb76259445b14e5a1f0e11008cb976aacfb23bada2e4346d7d4da81d2d2b-2024-05-14-10.2.52
  - persistentVolumeClaimRef:
      name: cluster-example-1-wal
    volumeSnapshotRef:
      name: snapshot-8b65beb9db8573e67e31c3120401d13f1ccd83bf4974b546970799ce0b0016c3-2024-05-14-10.2.52
  readyToUse: true
```

The corresponding `VolumeGroupSnapshotContent` will look like:

```
apiVersion: groupsnapshot.storage.k8s.io/v1alpha1
kind: VolumeGroupSnapshotContent
metadata:
  creationTimestamp: "2024-05-14T10:02:50Z"
  finalizers:
  - groupsnapshot.storage.kubernetes.io/volumegroupsnapshotcontent-bound-protection
  generation: 1
  name: groupsnapcontent-02f28293-ff7e-4d1c-8ea8-c64fb2b45a26
  resourceVersion: "64482"
  uid: 7671dd61-0af3-4e53-a389-d7528fca5260
spec:
  deletionPolicy: Delete
  driver: hostpath.csi.k8s.io
  source:
    volumeHandles:
    - f4416324-1165-11ef-91b4-22776546f286
    - f440f487-1165-11ef-91b4-22776546f286
  volumeGroupSnapshotClassName: csi-hostpath-groupsnapclass
  volumeGroupSnapshotRef:
    apiVersion: groupsnapshot.storage.k8s.io/v1alpha1
    kind: VolumeGroupSnapshot
    name: new-groupsnapshot
    namespace: default
    resourceVersion: "64440"
    uid: 02f28293-ff7e-4d1c-8ea8-c64fb2b45a26
status:
  creationTime: 1715680970153290824
  pvVolumeSnapshotContentList:
  - persistentVolumeRef:
      name: pvc-e92d8114-160f-4a59-8c29-bb6fb42e345f
    volumeSnapshotContentRef:
      name: snapcontent-219cbb76259445b14e5a1f0e11008cb976aacfb23bada2e4346d7d4da81d2d2b-2024-05-14-10.2.52
  - persistentVolumeRef:
      name: pvc-7b7b185e-2c35-4973-959e-15593d5f74a5
    volumeSnapshotContentRef:
      name: snapcontent-8b65beb9db8573e67e31c3120401d13f1ccd83bf4974b546970799ce0b0016c3-2024-05-14-10.2.52
  readyToUse: true
  volumeGroupSnapshotHandle: 1f3e6247-11d9-11ef-a7bc-b25da50ff257
```

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #

**Special notes for your reviewer**:


**Testing**

To test this patch, I used the [CSI host path driver](https://github.com/kubernetes-csi/csi-driver-host-path).
Having two PVCs and creating a VolumeGroupSnapshot matching them is enough to trigger the process.

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note
Link the snapshotted PVCs and the corresponding PVs in VolumeGroupSnaphot and VolumeGroupSnapshotContent to make restoring data easier.
```


k8s-ci-robot: Hi @leonardoce. Thanks for your PR.

I'm waiting for a [kubernetes-csi](https://github.com/orgs/kubernetes-csi/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-csi%2Fexternal-snapshotter).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-csi/external-snapshotter/pull/1069#" title="Author self-approved">leonardoce</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [xing-yang](https://github.com/xing-yang) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-csi%2Fexternal-snapshotter).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-csi/external-snapshotter/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["xing-yang"]} -->
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-csi/external-snapshotter/pull/1069#" title="Author self-approved">leonardoce</a>*, *<a href="https://github.com/kubernetes-csi/external-snapshotter/pull/1069#pullrequestreview-2008695293" title="Approved">sxd</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign [xing-yang](https://github.com/xing-yang) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-csi%2Fexternal-snapshotter).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-csi/external-snapshotter/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["xing-yang"]} -->
xing-yang: /ok-to-test
xing-yang: /assign
nixpanic: Once successfully created, maybe remove the `groupsnapshot.storage.kubernetes.io/info` annotation?
leonardoce: Thinking aloud, the `VolumeGroupSnapshotContent` resource would be more beautiful without that annotation...
xing-yang: /hold
xing-yang: I'd prefer the annotation gets removed.
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-csi/external-snapshotter/pull/1069#" title="Author self-approved">leonardoce</a>*, *<a href="https://github.com/kubernetes-csi/external-snapshotter/pull/1069#pullrequestreview-2008695293" title="Approved">sxd</a>*
**Once this PR has been reviewed and has the lgtm label**, please ask for approval from [xing-yang](https://github.com/xing-yang). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-csi%2Fexternal-snapshotter).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-csi/external-snapshotter/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["xing-yang"]} -->
leonardoce: I rebased it on the new master, removed the annotation, and updated the description of this PR with the latest resource definitions.

xing-yang: Can you update the PR description?
xing-yang: I see that the annotation is still in the code.  Can you please squash the commits?  It will be easier to see what code is removed this way.
leonardoce: You're correct, @xing-yang. 
The annotation is still used; it is only removed when the VolumeGroupSnapshot is marked ready.
By having that annotation, the patch avoids hitting the API server.

I've another implementation that doesn't use annotation and queries the API server when needed.
Do you prefer that one?
xing-yang: > You're correct, @xing-yang.
The annotation is still used; it is only removed when the VolumeGroupSnapshot is marked ready.
By having that annotation, the patch avoids hitting the API server.

> I've another implementation that doesn't use annotation and queries the API server when needed.
Do you prefer that one?

Yes, I'd prefer not to use the annotation.  Thanks.
leonardoce: Done @xing-yang. Thank you.

Patch:
diff --git a/pkg/common-controller/groupsnapshot_controller_helper.go b/pkg/common-controller/groupsnapshot_controller_helper.go
index c66ff4814..90752b71d 100644
--- a/pkg/common-controller/groupsnapshot_controller_helper.go
+++ b/pkg/common-controller/groupsnapshot_controller_helper.go
@@ -567,14 +567,43 @@ func (ctrl *csiSnapshotCommonController) updateGroupSnapshotStatus(groupSnapshot
 	var pvcVolumeSnapshotRefList []crdv1alpha1.PVCVolumeSnapshotPair
 	if groupSnapshotContent.Status != nil && len(groupSnapshotContent.Status.PVVolumeSnapshotContentList) != 0 {
 		for _, contentRef := range groupSnapshotContent.Status.PVVolumeSnapshotContentList {
-			groupSnapshotContent, err := ctrl.contentLister.Get(contentRef.VolumeSnapshotContentRef.Name)
+			var pvcReference v1.LocalObjectReference
+			pv, err := ctrl.client.CoreV1().PersistentVolumes().Get(context.TODO(), contentRef.PersistentVolumeRef.Name, metav1.GetOptions{})
+			if err != nil {
+				if apierrs.IsNotFound(err) {
+					klog.Errorf(
+						"updateGroupSnapshotStatus[%s]: PV [%s] not found",
+						utils.GroupSnapshotKey(groupSnapshot),
+						contentRef.PersistentVolumeRef.Name,
+					)
+				} else {
+					klog.Errorf(
+						"updateGroupSnapshotStatus[%s]: unable to get PV [%s] from the API server: %q",
+						utils.GroupSnapshotKey(groupSnapshot),
+						contentRef.PersistentVolumeRef.Name,
+						err.Error(),
+					)
+				}
+			} else {
+				if pv.Spec.ClaimRef != nil {
+					pvcReference.Name = pv.Spec.ClaimRef.Name
+				} else {
+					klog.Errorf(
+						"updateGroupSnapshotStatus[%s]: PV [%s] is not bound",
+						utils.GroupSnapshotKey(groupSnapshot),
+						contentRef.PersistentVolumeRef.Name)
+				}
+			}
+
+			volumeSnapshotContent, err := ctrl.contentLister.Get(contentRef.VolumeSnapshotContentRef.Name)
 			if err != nil {
 				return nil, fmt.Errorf("failed to get group snapshot content %s from group snapshot content store: %v", contentRef.VolumeSnapshotContentRef.Name, err)
 			}
 			pvcVolumeSnapshotRefList = append(pvcVolumeSnapshotRefList, crdv1alpha1.PVCVolumeSnapshotPair{
 				VolumeSnapshotRef: v1.LocalObjectReference{
-					Name: groupSnapshotContent.Spec.VolumeSnapshotRef.Name,
+					Name: volumeSnapshotContent.Spec.VolumeSnapshotRef.Name,
 				},
+				PersistentVolumeClaimRef: pvcReference,
 			})
 		}
 	}
diff --git a/pkg/sidecar-controller/groupsnapshot_helper.go b/pkg/sidecar-controller/groupsnapshot_helper.go
index c50bb13e6..1eaf4ec82 100644
--- a/pkg/sidecar-controller/groupsnapshot_helper.go
+++ b/pkg/sidecar-controller/groupsnapshot_helper.go
@@ -35,6 +35,13 @@ import (
 	"github.com/kubernetes-csi/external-snapshotter/v7/pkg/utils"
 )
 
+// snapshotContentNameVolumeHandlePair represent the link between a VolumeSnapshotContent and
+// the handle of the volume that was snapshotted
+type snapshotContentNameVolumeHandlePair struct {
+	snapshotContentName string
+	volumeHandle        string
+}
+
 func (ctrl *csiSnapshotSideCarController) storeGroupSnapshotContentUpdate(groupSnapshotContent interface{}) (bool, error) {
 	return utils.StoreObjectUpdate(ctrl.groupSnapshotContentStore, groupSnapshotContent, "groupsnapshotcontent")
 }
@@ -430,7 +437,7 @@ func (ctrl *csiSnapshotSideCarController) createGroupSnapshotWrapper(groupSnapsh
 		return groupSnapshotContent, fmt.Errorf("failed to get secret reference for group snapshot content %s: %v", groupSnapshotContent.Name, err)
 	}
 	// Create individual snapshots and snapshot contents
-	var snapshotContentNames []string
+	var snapshotContentLinks []snapshotContentNameVolumeHandlePair
 	for _, snapshot := range snapshots {
 		volumeSnapshotContentName := GetSnapshotContentNameForVolumeGroupSnapshotContent(string(groupSnapshotContent.UID), snapshot.SourceVolumeId)
 		volumeSnapshotName := GetSnapshotNameForVolumeGroupSnapshotContent(string(groupSnapshotContent.UID), snapshot.SourceVolumeId)
@@ -484,7 +491,10 @@ func (ctrl *csiSnapshotSideCarController) createGroupSnapshotWrapper(groupSnapsh
 		if err != nil {
 			return groupSnapshotContent, err
 		}
-		snapshotContentNames = append(snapshotContentNames, vsc.Name)
+		snapshotContentLinks = append(snapshotContentLinks, snapshotContentNameVolumeHandlePair{
+			snapshotContentName: vsc.Name,
+			volumeHandle:        snapshot.SourceVolumeId,
+		})
 
 		_, err = ctrl.clientset.SnapshotV1().VolumeSnapshots(volumeSnapshotNamespace).Create(context.TODO(), volumeSnapshot, metav1.CreateOptions{})
 		if err != nil {
@@ -497,7 +507,7 @@ func (ctrl *csiSnapshotSideCarController) createGroupSnapshotWrapper(groupSnapsh
 		}
 	}
 
-	newGroupSnapshotContent, err := ctrl.updateGroupSnapshotContentStatus(groupSnapshotContent, groupSnapshotID, readyToUse, creationTime.UnixNano(), snapshotContentNames)
+	newGroupSnapshotContent, err := ctrl.updateGroupSnapshotContentStatus(groupSnapshotContent, groupSnapshotID, readyToUse, creationTime.UnixNano(), snapshotContentLinks)
 	if err != nil {
 		klog.Errorf("error updating status for volume group snapshot content %s: %v.", groupSnapshotContent.Name, err)
 		return groupSnapshotContent, fmt.Errorf("error updating status for volume group snapshot content %s: %v", groupSnapshotContent.Name, err)
@@ -633,7 +643,8 @@ func (ctrl *csiSnapshotSideCarController) updateGroupSnapshotContentStatus(
 	groupSnapshotHandle string,
 	readyToUse bool,
 	createdAt int64,
-	snapshotContentNames []string) (*crdv1alpha1.VolumeGroupSnapshotContent, error) {
+	snapshotContentLinks []snapshotContentNameVolumeHandlePair,
+) (*crdv1alpha1.VolumeGroupSnapshotContent, error) {
 	klog.V(5).Infof("updateGroupSnapshotContentStatus: updating VolumeGroupSnapshotContent [%s], groupSnapshotHandle %s, readyToUse %v, createdAt %v", groupSnapshotContent.Name, groupSnapshotHandle, readyToUse, createdAt)
 
 	groupSnapshotContentObj, err := ctrl.clientset.GroupsnapshotV1alpha1().VolumeGroupSnapshotContents().Get(context.TODO(), groupSnapshotContent.Name, metav1.GetOptions{})
@@ -641,6 +652,11 @@ func (ctrl *csiSnapshotSideCarController) updateGroupSnapshotContentStatus(
 		return nil, fmt.Errorf("error get group snapshot content %s from api server: %v", groupSnapshotContent.Name, err)
 	}
 
+	pvs, err := ctrl.client.CoreV1().PersistentVolumes().List(context.TODO(), metav1.ListOptions{})
+	if err != nil {
+		return nil, fmt.Errorf("error get PersistentVolumes list from API server: %v", err)
+	}
+
 	var newStatus *crdv1alpha1.VolumeGroupSnapshotContentStatus
 	updated := false
 	if groupSnapshotContentObj.Status == nil {
@@ -649,10 +665,24 @@ func (ctrl *csiSnapshotSideCarController) updateGroupSnapshotContentStatus(
 			ReadyToUse:                &readyToUse,
 			CreationTime:              &createdAt,
 		}
-		for _, name := range snapshotContentNames {
+		for _, snapshotContentLink := range snapshotContentLinks {
+			pv := utils.GetPersistentVolumeFromHandle(pvs, groupSnapshotContent.Spec.Driver, snapshotContentLink.volumeHandle)
+			pvName := ""
+			if pv != nil {
+				pvName = pv.Name
+			} else {
+				klog.Errorf(
+					"updateGroupSnapshotContentStatus: unable to find PV for volumeHandle:[%s] and CSI driver:[%s]",
+					snapshotContentLink.volumeHandle,
+					groupSnapshotContent.Spec.Driver)
+			}
+
 			newStatus.PVVolumeSnapshotContentList = append(newStatus.PVVolumeSnapshotContentList, crdv1alpha1.PVVolumeSnapshotContentPair{
 				VolumeSnapshotContentRef: v1.LocalObjectReference{
-					Name: name,
+					Name: snapshotContentLink.snapshotContentName,
+				},
+				PersistentVolumeRef: v1.LocalObjectReference{
+					Name: pvName,
 				},
 			})
 		}
@@ -675,10 +705,24 @@ func (ctrl *csiSnapshotSideCarController) updateGroupSnapshotContentStatus(
 			updated = true
 		}
 		if len(newStatus.PVVolumeSnapshotContentList) == 0 {
-			for _, name := range snapshotContentNames {
+			for _, snapshotContentLink := range snapshotContentLinks {
+				pv := utils.GetPersistentVolumeFromHandle(pvs, groupSnapshotContent.Spec.Driver, snapshotContentLink.volumeHandle)
+				pvName := ""
+				if pv != nil {
+					pvName = pv.Name
+				} else {
+					klog.Errorf(
+						"updateGroupSnapshotContentStatus: unable to find PV for volumeHandle:[%s] and CSI driver:[%s] (existing status)",
+						snapshotContentLink.volumeHandle,
+						groupSnapshotContent.Spec.Driver)
+				}
+
 				newStatus.PVVolumeSnapshotContentList = append(newStatus.PVVolumeSnapshotContentList, crdv1alpha1.PVVolumeSnapshotContentPair{
 					VolumeSnapshotContentRef: v1.LocalObjectReference{
-						Name: name,
+						Name: snapshotContentLink.snapshotContentName,
+					},
+					PersistentVolumeRef: v1.LocalObjectReference{
+						Name: pvName,
 					},
 				})
 			}
@@ -842,7 +886,7 @@ func (ctrl *csiSnapshotSideCarController) checkandUpdateGroupSnapshotContentStat
 		}
 
 		// TODO: Get a reference to snapshot contents for this volume group snapshot
-		updatedContent, err := ctrl.updateGroupSnapshotContentStatus(groupSnapshotContent, groupSnapshotID, readyToUse, creationTime.UnixNano(), []string{})
+		updatedContent, err := ctrl.updateGroupSnapshotContentStatus(groupSnapshotContent, groupSnapshotID, readyToUse, creationTime.UnixNano(), []snapshotContentNameVolumeHandlePair{})
 		if err != nil {
 			return groupSnapshotContent, err
 		}
diff --git a/pkg/utils/pvs.go b/pkg/utils/pvs.go
new file mode 100644
index 000000000..675488a37
--- /dev/null
+++ b/pkg/utils/pvs.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import v1 "k8s.io/api/core/v1"
+
+// GetPersistentVolumeFromHandle looks for the PV having a certain CSI driver name
+// and corresponding to a volume with a given handle, in a PV List.
+// If the PV is not found, returns nil
+func GetPersistentVolumeFromHandle(pvList *v1.PersistentVolumeList, driverName, volumeHandle string) *v1.PersistentVolume {
+	for i := range pvList.Items {
+		if pvList.Items[i].Spec.CSI == nil {
+			continue
+		}
+
+		if pvList.Items[i].Spec.CSI.Driver == driverName && pvList.Items[i].Spec.CSI.VolumeHandle == volumeHandle {
+			return &pvList.Items[i]
+		}
+	}
+
+	return nil
+}


Test patch:
diff --git a/pkg/utils/pvs_test.go b/pkg/utils/pvs_test.go
new file mode 100644
index 000000000..f7fff7ff0
--- /dev/null
+++ b/pkg/utils/pvs_test.go
@@ -0,0 +1,106 @@
+/*
+Copyright 2024 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestGetPersistentVolumeFromHandle(t *testing.T) {
+	testDriverName := "hostpath.csi.k8s.io"
+	testVolumeHandle := "df39ea9e-1296-11ef-adde-baf37ed30dae"
+	testPvName := "pv-name"
+
+	pvListTest := v1.PersistentVolumeList{
+		Items: []v1.PersistentVolume{
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: testPvName,
+				},
+				Spec: v1.PersistentVolumeSpec{
+					PersistentVolumeSource: v1.PersistentVolumeSource{
+						CSI: &v1.CSIPersistentVolumeSource{
+							Driver:       testDriverName,
+							VolumeHandle: testVolumeHandle,
+						},
+					},
+				},
+			},
+			{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "pv-no-csi",
+				},
+				Spec: v1.PersistentVolumeSpec{
+					PersistentVolumeSource: v1.PersistentVolumeSource{
+						HostPath: &v1.HostPathVolumeSource{},
+					},
+				},
+			},
+		},
+	}
+
+	tests := []struct {
+		testName     string
+		driverName   string
+		volumeHandle string
+		pvList       v1.PersistentVolumeList
+		pvName       string
+	}{
+		{
+			testName:     "empty-pv-list",
+			driverName:   testDriverName,
+			volumeHandle: testVolumeHandle,
+			pvName:       "",
+		},
+		{
+			testName:     "pv-in-list",
+			driverName:   testDriverName,
+			volumeHandle: testVolumeHandle,
+			pvList:       pvListTest,
+			pvName:       testPvName,
+		},
+		{
+			testName:     "not-existing-volume-handle",
+			driverName:   testDriverName,
+			volumeHandle: "not-existing-volume-handle",
+			pvList:       pvListTest,
+			pvName:       "",
+		},
+		{
+			testName:     "invalid-driver-name",
+			driverName:   "invalid-driver-name",
+			volumeHandle: testVolumeHandle,
+			pvList:       pvListTest,
+			pvName:       "",
+		},
+	}
+	for _, tt := range tests {
+		got := GetPersistentVolumeFromHandle(&tt.pvList, tt.driverName, tt.volumeHandle)
+		if got == nil {
+			if len(tt.pvName) != 0 {
+				t.Errorf("%v: GetPersistentVolumeFromHandle = %v WANT %v", tt.testName, got, tt.pvName)
+			}
+		} else {
+			if tt.pvName != got.Name {
+				t.Errorf("%v: GetPersistentVolumeFromHandle = %v WANT %v", tt.testName, got.Name, tt.pvName)
+			}
+		}
+	}
+}