Repository: kubernetes-sigs/aws-ebs-csi-driver
Pull request #1558 (2023-04-04 21:20:23)

Support interpolated snapshot tags
**Is this a bug fix or adding new feature?**
New feature. 

**What is this PR about? / Why do we need it?**
For more details, see issue #1553 
1. Added support for interpolated snapshot tags that are dynamically computed at runtime using ``VolumeSnapshot`` name, namespace and ``VolumeSnapshotContent`` name.
2. Modified the helm chart template ``controller.yaml`` to allow setting ``--extra-create-metadata`` for ``csi-snapshotter`` sidecar. It enables ``external-snapshotter`` to pass the snapshot parameters to CSI Driver.
3. Added the documentation for Snapshot Tagging.

**What testing is done?** 
1. Added unit tests of string interpolation on the VolumeSnapshot template.
2. E2E tests. Deployed the Driver with the changes. Provisioned a snapshot using this VolumeSnapshotClass
```
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: csi-aws-vsc
driver: ebs.csi.aws.com
deletionPolicy: Delete
parameters:
  tagSpecification_1: "key1={{ .VolumeSnapshotNamespace }}"
  tagSpecification_2: "key2={{ .VolumeSnapshotName }}"
  tagSpecification_3: "key3={{ .VolumeSnapshotContentName }}"
  tagSpecification_4: 'key4={{ .VolumeSnapshotName |  contains "ebs"}}'
```
and this VolumeSnapshot
```
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: ebs-volume-snapshot
spec:
  volumeSnapshotClassName: csi-aws-vsc
  source:
    persistentVolumeClaimName: ebs-claim
```
The tags for the snapshot in the AWS EC2 console were created as expected:
```
key2 | ebs-volume-snapshot
key1 | default
key3 | snapcontent-97516503-aa32-4fc5-96ae-3e694d2519d7
key4 | true
```


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

This pull-request has been approved by:
**Once this PR has been reviewed and has the lgtm label**, please assign wongma7 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-sigs%2Faws-ebs-csi-driver).

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

- **[OWNERS](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/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":["wongma7"]} -->
k8s-ci-robot: Hi @hanyuel. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/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-sigs%2Faws-ebs-csi-driver).

<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>

ConnorJC3: /ok-to-test
k8s-ci-robot: @hanyuel: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-aws-ebs-csi-driver-verify | 247e8a6a8e00b8fbf1da9da18679ad53c14534a8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-verify/1643365571565195264) | true | `/test pull-aws-ebs-csi-driver-verify`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=aws-ebs-csi-driver&pr=1558). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ahanyuel). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues?q=is:issue+is:open) when you hit one in your PR.

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @hanyuel: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-aws-ebs-csi-driver-verify | 247e8a6a8e00b8fbf1da9da18679ad53c14534a8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-verify/1643365571565195264) | true | `/test pull-aws-ebs-csi-driver-verify`
pull-aws-ebs-csi-driver-test-helm-chart | 247e8a6a8e00b8fbf1da9da18679ad53c14534a8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-test-helm-chart/1643365571506475008) | true | `/test pull-aws-ebs-csi-driver-test-helm-chart`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=aws-ebs-csi-driver&pr=1558). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ahanyuel). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues?q=is:issue+is:open) when you hit one in your PR.

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
k8s-ci-robot: @hanyuel: The following tests **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-aws-ebs-csi-driver-verify | 247e8a6a8e00b8fbf1da9da18679ad53c14534a8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-verify/1643365571565195264) | true | `/test pull-aws-ebs-csi-driver-verify`
pull-aws-ebs-csi-driver-test-helm-chart | 247e8a6a8e00b8fbf1da9da18679ad53c14534a8 | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-test-helm-chart/1643614111356948480) | unknown | `/test pull-aws-ebs-csi-driver-test-helm-chart`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=aws-ebs-csi-driver&pr=1558). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ahanyuel). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues?q=is:issue+is:open) when you hit one in your PR.

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
torredil: /retest
k8s-ci-robot: @hanyuel: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
pull-aws-ebs-csi-driver-test-helm-chart | 71b2d92bfb80ebaa83853643039160d62b9a66af | [link](https://prow.k8s.io/view/gs/kubernetes-jenkins/pr-logs/pull/kubernetes-sigs_aws-ebs-csi-driver/1558/pull-aws-ebs-csi-driver-test-helm-chart/1643676899890696192) | true | `/test pull-aws-ebs-csi-driver-test-helm-chart`

[Full PR test history](https://prow.k8s.io/pr-history?org=kubernetes-sigs&repo=aws-ebs-csi-driver&pr=1558). [Your PR dashboard](https://prow.k8s.io/pr?query=is%3Apr%20state%3Aopen%20author%3Ahanyuel). Please help us cut down on flakes by [linking to](https://git.k8s.io/community/contributors/devel/sig-testing/flaky-tests.md#filing-issues-for-flaky-tests) an [open issue](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/issues?q=is:issue+is:open) when you hit one in your PR.

<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. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
torredil: Waiting for https://github.com/kubernetes-sigs/aws-ebs-csi-driver/pull/1560 to land, fixes `pull-aws-ebs-csi-driver-test-helm-chart`.
torredil: /lgtm
k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/aws-ebs-csi-driver/pull/1558#pullrequestreview-1375480693" title="Approved">gtxu</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/OWNERS)~~ [gtxu]

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":[]} -->
torredil: /hold will let the PR author unhold when everything is good to go.
k8s-ci-robot: New changes are detected. LGTM label has been removed.
rdpsin: /lgtm

Patch:
diff --git a/charts/aws-ebs-csi-driver/templates/controller.yaml b/charts/aws-ebs-csi-driver/templates/controller.yaml
index 28eb4636..0d79331a 100644
--- a/charts/aws-ebs-csi-driver/templates/controller.yaml
+++ b/charts/aws-ebs-csi-driver/templates/controller.yaml
@@ -268,6 +268,9 @@ spec:
           args:
             - --csi-address=$(ADDRESS)
             - --leader-election=true
+            {{- if .Values.controller.extraCreateMetadata }}
+            - --extra-create-metadata
+            {{- end}}
           env:
             - name: ADDRESS
               value: /var/lib/csi/sockets/pluginproxy/csi.sock
diff --git a/deploy/kubernetes/base/controller.yaml b/deploy/kubernetes/base/controller.yaml
index bd34f607..f1f1cf75 100644
--- a/deploy/kubernetes/base/controller.yaml
+++ b/deploy/kubernetes/base/controller.yaml
@@ -184,6 +184,7 @@ spec:
           args:
             - --csi-address=$(ADDRESS)
             - --leader-election=true
+            - --extra-create-metadata
           env:
             - name: ADDRESS
               value: /var/lib/csi/sockets/pluginproxy/csi.sock
diff --git a/docs/tagging.md b/docs/tagging.md
index d900b4b9..5b22a78e 100644
--- a/docs/tagging.md
+++ b/docs/tagging.md
@@ -96,6 +96,40 @@ backup=true
 billingID=ABCDEF
 ```
 
+# Snapshot Tagging
+The AWS EBS CSI Driver supports tagging snapshots through `VolumeSnapshotClass.parameters`, similarly to StorageClass tagging.
+
+The CSI driver supports runtime string interpolation on the snapshot tag values. You can specify placeholders for VolumeSnapshot namespace, VolumeSnapshot name and VolumeSnapshotContent name, which will then be dynamically computed at runtime. You can also use the functions provided by the CSI Driver to apply more expressive tags. **Note: Interpolated tags require the `--extra-create-metadata` flag to be enabled on the `external-snapshotter` sidecar.**
+
+**Example**
+```
+apiVersion: snapshot.storage.k8s.io/v1
+kind: VolumeSnapshotClass
+metadata:
+  name: csi-aws-vsc
+driver: ebs.csi.aws.com
+deletionPolicy: Delete
+parameters:
+  tagSpecification_1: "key1=value1"
+  tagSpecification_2: "key2="
+  # Interpolated tag
+  tagSpecification_3: "snapshotnamespace={{ .VolumeSnapshotNamespace }}"
+  tagSpecification_4: "snapshotname={{ .VolumeSnapshotName }}"
+  tagSpecification_5: "snapshotcontentname={{ .VolumeSnapshotContentName }}"
+  # Interpolated tag w/ function
+  tagSpecification_6: 'key6={{ .VolumeSnapshotNamespace | contains "prod" }}'
+```
+
+Provisioning a snapshot in namespace 'ns-prod' with `VolumeSnapshot` name being 'ebs-snapshot' using this VolumeSnapshotClass, will apply the following tags to the snapshot:
+
+```
+key1=value1
+key2=<empty string>
+snapshotnamespace=ns-prod
+snapshotname=ebs-snapshot
+snapshotcontentname=<the computed VolumeSnapshotContent name>
+key6=true
+```
 ____
 
 ## Failure Modes
diff --git a/pkg/driver/constants.go b/pkg/driver/constants.go
index 5b262259..b3032004 100644
--- a/pkg/driver/constants.go
+++ b/pkg/driver/constants.go
@@ -69,6 +69,16 @@ const (
 	// provisioned volume
 	PVNameKey = "csi.storage.k8s.io/pv/name"
 
+	// VolumeSnapshotNameKey contains name of the snapshot
+	VolumeSnapshotNameKey = "csi.storage.k8s.io/volumesnapshot/name"
+
+	// VolumeSnapshotNamespaceKey contains namespace of the snapshot
+	VolumeSnapshotNamespaceKey = "csi.storage.k8s.io/volumesnapshot/namespace"
+
+	// VolumeSnapshotCotentNameKey contains name of the VolumeSnapshotContent that is the source
+	// for the snapshot
+	VolumeSnapshotContentNameKey = "csi.storage.k8s.io/volumesnapshotcontent/name"
+
 	// BlockExpressKey increases the iops limit for io2 volumes to the block express limit
 	BlockExpressKey = "blockexpress"
 
diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go
index aa6093ee..65aeccbc 100644
--- a/pkg/driver/controller.go
+++ b/pkg/driver/controller.go
@@ -135,7 +135,7 @@ func (d *controllerService) CreateVolume(ctx context.Context, req *csi.CreateVol
 		blockSize string
 	)
 
-	tProps := new(template.Props)
+	tProps := new(template.PVProps)
 
 	for key, value := range req.GetParameters() {
 		switch strings.ToLower(key) {
@@ -605,15 +605,25 @@ func (d *controllerService) CreateSnapshot(ctx context.Context, req *csi.CreateS
 	}
 
 	var vscTags []string
+	vsProps := new(template.VolumeSnapshotProps)
 	for key, value := range req.GetParameters() {
-		if strings.HasPrefix(key, TagKeyPrefix) {
-			vscTags = append(vscTags, value)
-		} else {
-			return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter key %s for CreateSnapshot", key)
+		switch key {
+		case VolumeSnapshotNameKey:
+			vsProps.VolumeSnapshotName = value
+		case VolumeSnapshotNamespaceKey:
+			vsProps.VolumeSnapshotNamespace = value
+		case VolumeSnapshotContentNameKey:
+			vsProps.VolumeSnapshotContentName = value
+		default:
+			if strings.HasPrefix(key, TagKeyPrefix) {
+				vscTags = append(vscTags, value)
+			} else {
+				return nil, status.Errorf(codes.InvalidArgument, "Invalid parameter key %s for CreateSnapshot", key)
+			}
 		}
 	}
 
-	addTags, err := template.Evaluate(vscTags, nil, d.driverOptions.warnOnInvalidTag)
+	addTags, err := template.Evaluate(vscTags, vsProps, d.driverOptions.warnOnInvalidTag)
 	if err != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "Error interpolating the tag value: %v", err)
 	}
diff --git a/pkg/driver/mount_windows.go b/pkg/driver/mount_windows.go
index 8824f3db..c9a9e1e4 100644
--- a/pkg/driver/mount_windows.go
+++ b/pkg/driver/mount_windows.go
@@ -56,7 +56,7 @@ func (m NodeMounter) GetDeviceNameFromMount(mountPath string) (string, int, erro
 		// internal Get-Item cmdlet didn't fail but no item/device was found at the
 		// path so we should return empty string and nil error just like the Linux
 		// implementation would.
-		pattern := `(Get-Item -Path \S+).Target, output: , error: <nil>|because it does not exist`
+		pattern := `(Get-Item -Path \S+).Target, output: , error: <nil>`
 		matched, matchErr := regexp.MatchString(pattern, err.Error())
 		if matched {
 			return "", 0, nil
@@ -128,6 +128,8 @@ func (m *NodeMounter) Unpublish(target string) error {
 	if !ok {
 		return fmt.Errorf("failed to cast mounter to csi proxy mounter")
 	}
+	// WriteVolumeCache before unmount
+	proxyMounter.WriteVolumeCache(target)
 	// Remove symlink
 	err := proxyMounter.Rmdir(target)
 	if err != nil {
@@ -148,6 +150,11 @@ func (m *NodeMounter) Unstage(target string) error {
 	if err != nil {
 		return err
 	}
+	// Cleanup stage path
+	err = proxyMounter.Rmdir(target)
+	if err != nil {
+		return err
+	}
 	return nil
 }
 
diff --git a/pkg/driver/node.go b/pkg/driver/node.go
index 22a43a47..50e2017d 100644
--- a/pkg/driver/node.go
+++ b/pkg/driver/node.go
@@ -220,14 +220,13 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
 	// This operation (NodeStageVolume) MUST be idempotent.
 	// If the volume corresponding to the volume_id is already staged to the staging_target_path,
 	// and is identical to the specified volume_capability the Plugin MUST reply 0 OK.
-	klog.V(4).InfoS("NodeStageVolume: checking if volume is already staged", "device", device, "source", source, "target", target)
 	if device == source {
 		klog.V(4).InfoS("NodeStageVolume: volume already staged", "volumeID", volumeID)
 		return &csi.NodeStageVolumeResponse{}, nil
 	}
 
 	// FormatAndMount will format only if needed
-	klog.V(4).InfoS("NodeStageVolume: formatting and mounting with fstype", "source", source, "volumeID", volumeID, "target", target, "fstype", fsType)
+	klog.V(4).InfoS("NodeStageVolume: formatting and mounting with fstype", "source", source, "target", target, "fstype", fsType)
 	formatOptions := []string{}
 	if len(blockSize) > 0 {
 		if fsType == FSTypeXfs {
@@ -256,7 +255,6 @@ func (d *nodeService) NodeStageVolume(ctx context.Context, req *csi.NodeStageVol
 			return nil, status.Errorf(codes.Internal, "Could not resize volume %q (%q):  %v", volumeID, source, err)
 		}
 	}
-	klog.V(4).InfoS("NodeStageVolume: successfully formatted and mounted volume", "source", source, "volumeID", volumeID, "target", target, "fstype", fsType)
 	return &csi.NodeStageVolumeResponse{}, nil
 }
 
@@ -285,7 +283,7 @@ func (d *nodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstag
 	// returns the device name, reference count, and error code
 	dev, refCount, err := d.mounter.GetDeviceNameFromMount(target)
 	if err != nil {
-		msg := fmt.Sprintf("failed to check if target %q is a mount point: %v", target, err)
+		msg := fmt.Sprintf("failed to check if volume is mounted: %v", err)
 		return nil, status.Error(codes.Internal, msg)
 	}
 
@@ -306,7 +304,7 @@ func (d *nodeService) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstag
 	if err != nil {
 		return nil, status.Errorf(codes.Internal, "Could not unmount target %q: %v", target, err)
 	}
-	klog.V(4).InfoS("NodeUnStageVolume: successfully unstaged volume", "volumeID", volumeID, "target", target)
+
 	return &csi.NodeUnstageVolumeResponse{}, nil
 }
 
diff --git a/pkg/mounter/safe_mounter_windows.go b/pkg/mounter/safe_mounter_windows.go
index 81ef3aa6..a8b6163e 100644
--- a/pkg/mounter/safe_mounter_windows.go
+++ b/pkg/mounter/safe_mounter_windows.go
@@ -97,13 +97,11 @@ func (mounter *CSIProxyMounter) Unmount(target string) error {
 		TargetPath: normalizeWindowsPath(target),
 	}
 	volumeIdResponse, err := mounter.VolumeClient.GetVolumeIDFromTargetPath(context.Background(), getVolumeIdRequest)
-	if err != nil {
-		return err
-	}
+	volumeId := volumeIdResponse.GetVolumeId()
 
 	// Call UnmountVolume CSI proxy function which flushes data cache to disk and removes the global staging path
 	unmountVolumeRequest := &volume.UnmountVolumeRequest{
-		VolumeId:   volumeIdResponse.VolumeId,
+		VolumeId:   volumeId,
 		TargetPath: normalizeWindowsPath(target),
 	}
 	_, err = mounter.VolumeClient.UnmountVolume(context.Background(), unmountVolumeRequest)
@@ -111,31 +109,25 @@ func (mounter *CSIProxyMounter) Unmount(target string) error {
 		return err
 	}
 
-	// Cleanup stage path
-	err = mounter.Rmdir(target)
-	if err != nil {
-		return err
-	}
-
 	// Get disk number
 	getDiskNumberRequest := &volume.GetDiskNumberFromVolumeIDRequest{
-		VolumeId: volumeIdResponse.VolumeId,
+		VolumeId: volumeId,
 	}
 	getDiskNumberResponse, err := mounter.VolumeClient.GetDiskNumberFromVolumeID(context.Background(), getDiskNumberRequest)
+	diskNumber := getDiskNumberResponse.GetDiskNumber()
 	if err != nil {
 		return err
 	}
 
 	// Offline the disk
 	setDiskStateRequest := &disk.SetDiskStateRequest{
-		DiskNumber: getDiskNumberResponse.DiskNumber,
+		DiskNumber: diskNumber,
 		IsOnline:   false,
 	}
 	_, err = mounter.DiskClient.SetDiskState(context.Background(), setDiskStateRequest)
 	if err != nil {
 		return err
 	}
-	klog.V(4).InfoS("Successfully unmounted volume", "diskNumber", getDiskNumberResponse.DiskNumber, "volumeId", volumeIdResponse.VolumeId, "target", target)
 	return nil
 }
 
@@ -216,23 +208,15 @@ func (mounter *CSIProxyMounter) DeviceOpened(pathname string) (bool, error) {
 	return false, fmt.Errorf("DeviceOpened not implemented for CSIProxyMounter")
 }
 
-// GetDeviceNameFromMount returns the disk number for a mount path.
-func (mounter *CSIProxyMounter) GetDeviceNameFromMount(mountPath, _ string) (string, error) {
+// GetDeviceNameFromMount returns the volume ID for a mount path.
+func (mounter *CSIProxyMounter) GetDeviceNameFromMount(mountPath, pluginMountDir string) (string, error) {
 	req := &volume.GetVolumeIDFromTargetPathRequest{TargetPath: normalizeWindowsPath(mountPath)}
 	resp, err := mounter.VolumeClient.GetVolumeIDFromTargetPath(context.Background(), req)
 	if err != nil {
 		return "", err
 	}
-	// Get disk number
-	getDiskNumberRequest := &volume.GetDiskNumberFromVolumeIDRequest{
-		VolumeId: resp.VolumeId,
-	}
-	getDiskNumberResponse, err := mounter.VolumeClient.GetDiskNumberFromVolumeID(context.Background(), getDiskNumberRequest)
-	if err != nil {
-		return "", err
-	}
-	klog.V(4).InfoS("GetDeviceNameFromMount called", "diskNumber", getDiskNumberResponse.DiskNumber, "volumeID", resp.VolumeId, "mountPath", mountPath)
-	return fmt.Sprint(getDiskNumberResponse.DiskNumber), nil
+
+	return resp.VolumeId, nil
 }
 
 func (mounter *CSIProxyMounter) MakeRShared(path string) error {
diff --git a/pkg/util/template/template.go b/pkg/util/template/template.go
index 67d5fa5a..0d9cd63e 100644
--- a/pkg/util/template/template.go
+++ b/pkg/util/template/template.go
@@ -8,13 +8,19 @@ import (
 	"k8s.io/klog/v2"
 )
 
-type Props struct {
+type PVProps struct {
 	PVCName      string
 	PVCNamespace string
 	PVName       string
 }
 
-func Evaluate(tm []string, props *Props, warnOnly bool) (map[string]string, error) {
+type VolumeSnapshotProps struct {
+	VolumeSnapshotName        string
+	VolumeSnapshotNamespace   string
+	VolumeSnapshotContentName string
+}
+
+func Evaluate(tm []string, props interface{}, warnOnly bool) (map[string]string, error) {
 	md := make(map[string]string)
 	for _, s := range tm {
 		st := strings.SplitN(s, "=", 2)
@@ -39,7 +45,7 @@ func Evaluate(tm []string, props *Props, warnOnly bool) (map[string]string, erro
 	return md, nil
 }
 
-func execTemplate(value string, props *Props, t *template.Template) (string, error) {
+func execTemplate(value string, props interface{}, t *template.Template) (string, error) {
 	tmpl, err := t.Parse(value)
 	if err != nil {
 		return "", err


Test patch:
diff --git a/pkg/util/template/template_test.go b/pkg/util/template/template_test.go
index a11fef67..9b7ce76e 100644
--- a/pkg/util/template/template_test.go
+++ b/pkg/util/template/template_test.go
@@ -189,7 +189,7 @@ func TestEvaluate(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 
-			props := &Props{
+			props := &PVProps{
 				PVCName:      tc.pvcName,
 				PVCNamespace: tc.pvcNamespace,
 				PVName:       tc.pvName,
@@ -212,3 +212,144 @@ func TestEvaluate(t *testing.T) {
 		})
 	}
 }
+
+func TestEvaluateVolumeSnapshotTemplate(t *testing.T) {
+	testCases := []struct {
+		name                      string
+		input                     []string
+		volumeSnapshotName        string
+		volumeSnapshotNamespace   string
+		volumeSnapshotContentName string
+		warnOnly                  bool
+		expectErr                 bool
+		expectedTags              map[string]string
+	}{
+		{
+			name: "simple substitution",
+			input: []string{
+				"key1={{ .VolumeSnapshotName }}",
+				"key2={{ .VolumeSnapshotNamespace }}",
+				"key3={{ .VolumeSnapshotContentName }}",
+			},
+			volumeSnapshotName:        "ebs-vs",
+			volumeSnapshotNamespace:   "default",
+			volumeSnapshotContentName: "ebs-vs-content-012345",
+			expectedTags: map[string]string{
+				"key1": "ebs-vs",
+				"key2": "default",
+				"key3": "ebs-vs-content-012345",
+			},
+		},
+		{
+			name: "template parsing error",
+			input: []string{
+				"key1={{ .VolumeSnapshotName }",
+			},
+			expectErr: true,
+		},
+		{
+			name: "template parsing error warn only",
+			input: []string{
+				"key1={{ .VolumeSnapshotName }",
+				"key2={{ .VolumeSnapshotNamespace }}",
+			},
+			volumeSnapshotName:      "ebs-vs",
+			volumeSnapshotNamespace: "default",
+			warnOnly:                true,
+			expectedTags: map[string]string{
+				"key2": "default",
+			},
+		},
+		{
+			name: "test unsupported func - returns error",
+			input: []string{
+				`backup={{ .VolumeSnapshotNamespace | html }}`,
+			},
+			volumeSnapshotNamespace: "ns-prod",
+			expectErr:               true,
+		},
+		{
+			name: "test function - contains",
+			input: []string{
+				`backup={{ .VolumeSnapshotNamespace | contains "prod" }}`,
+			},
+			volumeSnapshotNamespace: "ns-prod",
+			expectedTags: map[string]string{
+				"backup": "true",
+			},
+		},
+		{
+			name: "test function - toUpper",
+			input: []string{
+				`backup={{ .VolumeSnapshotNamespace | toUpper }}`,
+			},
+			volumeSnapshotNamespace: "ns-prod",
+			expectedTags: map[string]string{
+				"backup": "NS-PROD",
+			},
+		},
+		{
+			name: "test function - toLower",
+			input: []string{
+				`backup={{ .VolumeSnapshotNamespace | toLower }}`,
+			},
+			volumeSnapshotNamespace: "ns-PROD",
+			expectedTags: map[string]string{
+				"backup": "ns-prod",
+			},
+		},
+		{
+			name: "test function - field",
+			input: []string{
+				`backup={{ .VolumeSnapshotNamespace | field "-" 1 }}`,
+			},
+			volumeSnapshotNamespace: "ns-prod-default",
+			expectedTags: map[string]string{
+				"backup": "prod",
+			},
+		},
+		{
+			name: "test function - substring",
+			input: []string{
+				`key1={{ .VolumeSnapshotNamespace | substring 0 4 }}`,
+			},
+			volumeSnapshotNamespace: "prod-12345",
+			expectedTags: map[string]string{
+				"key1": "prod",
+			},
+		},
+		{
+			name: "field returns error",
+			input: []string{
+				`key1={{ .VolumeSnapshotNamespace | field "-" 1 }}`,
+			},
+			expectErr: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+
+			props := &VolumeSnapshotProps{
+				VolumeSnapshotName:        tc.volumeSnapshotName,
+				VolumeSnapshotNamespace:   tc.volumeSnapshotNamespace,
+				VolumeSnapshotContentName: tc.volumeSnapshotContentName,
+			}
+
+			tags, err := Evaluate(tc.input, props, tc.warnOnly)
+
+			if tc.expectErr {
+				if err == nil {
+					t.Fatalf("expected an error; got nil")
+				}
+			} else {
+				if err != nil {
+					t.Fatalf("err is not nil; err = %v", err)
+				}
+				if diff := cmp.Diff(tc.expectedTags, tags); diff != "" {
+					t.Fatalf("tags are different; diff = %v", diff)
+				}
+			}
+		})
+	}
+}