Repository: kubernetes-sigs/gcp-filestore-csi-driver
Pull request #273 (2022-05-20 04:42:05)

Cherrypick #266, #267, #268, #269, #270, #271, #272 to release-1.2
<!--  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**:
Cherrypick #266, #267, #268, #269, #270, #271, #272 to release-1.2

**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**:

**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
Cherrypick #266, #267, #268, #269, #270, #271, #272 to release-1.2
```


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

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/273#" title="Author self-approved">saikat-royc</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fgcp-filestore-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/gcp-filestore-csi-driver/blob/release-1.2/OWNERS)~~ [saikat-royc]

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":[]} -->

Patch:
diff --git a/CHANGELOG/CHANGELOG-1.2.md b/CHANGELOG/CHANGELOG-1.2.md
index 941ab616..a54187d3 100644
--- a/CHANGELOG/CHANGELOG-1.2.md
+++ b/CHANGELOG/CHANGELOG-1.2.md
@@ -1,3 +1,31 @@
+# v1.2.1 - Changelog since v1.2.0
+
+## Changes by Kind
+
+### Feature
+- Multishare volume expansion ([#270](https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/270), [@leiyiz](https://github.com/leiyiz))
+- List instances to list shares for multishare instance ([#271](https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/271), [@leiyiz](https://github.com/leiyiz))
+
+### Bug or Regression
+
+- Align instance resize target bytes ([#269](https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/269), [@saikat-royc](https://github.com/saikat-royc))
+
+### Uncategorized
+
+- Add test for NodeGetVolumeStats in resize e2e test. ([#268](https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/268), [@tyuchn](https://github.com/tyuchn))
+- Expose volume metrics by implementing NodeGetVolumeStats for NodeServer. ([#266](https://github.com/kubernetes-sigs/gcp-filestore-csi-driver/pull/266), [@tyuchn](https://github.com/tyuchn))
+
+## Dependencies
+
+### Added
+_Nothing has changed._
+
+### Changed
+_Nothing has changed._
+
+### Removed
+_Nothing has changed._
+
 # v1.2.0 - Changelog since v1.1.4
 
 ## Changes by Kind
diff --git a/deploy/kubernetes/base/controller/kustomization.yaml b/deploy/kubernetes/base/controller/kustomization.yaml
index 943612e4..9b8a09b3 100644
--- a/deploy/kubernetes/base/controller/kustomization.yaml
+++ b/deploy/kubernetes/base/controller/kustomization.yaml
@@ -6,4 +6,3 @@ resources:
 - setup_cluster.yaml
 - controller.yaml
 - csi_driver.yaml
-- psp.yaml
diff --git a/deploy/kubernetes/base/controller/psp.yaml b/deploy/kubernetes/base/controller/psp.yaml
deleted file mode 100644
index 5b63e185..00000000
--- a/deploy/kubernetes/base/controller/psp.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: gcp-filestore-csi-controller-psp
-spec:
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  fsGroup:
-    rule: RunAsAny
-  volumes:
-    - "emptyDir"
-    - "secret"
-  hostNetwork: true
diff --git a/deploy/kubernetes/base/controller/setup_cluster.yaml b/deploy/kubernetes/base/controller/setup_cluster.yaml
index dde6bb36..365782c7 100644
--- a/deploy/kubernetes/base/controller/setup_cluster.yaml
+++ b/deploy/kubernetes/base/controller/setup_cluster.yaml
@@ -149,18 +149,6 @@ roleRef:
   apiGroup: rbac.authorization.k8s.io
 ---
 
-kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1
-metadata:
-  name: gcp-filestore-csi-controller-deploy
-rules:
-  - apiGroups: ["policy"]
-    resources: ["podsecuritypolicies"]
-    verbs: ["use"]
-    resourceNames:
-      - gcp-filestore-csi-controller-psp
----
-
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
@@ -175,18 +163,6 @@ subjects:
     namespace: gcp-filestore-csi-driver
 ---
 
-kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1
-metadata:
-  name: gcp-filestore-csi-node-deploy
-rules:
-  - apiGroups: ['policy']
-    resources: ['podsecuritypolicies']
-    verbs:     ['use']
-    resourceNames:
-    - gcp-filestore-csi-node-psp
----
-
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
diff --git a/deploy/kubernetes/base/node_linux/kustomization.yaml b/deploy/kubernetes/base/node_linux/kustomization.yaml
index 90c84250..b8d26735 100644
--- a/deploy/kubernetes/base/node_linux/kustomization.yaml
+++ b/deploy/kubernetes/base/node_linux/kustomization.yaml
@@ -4,4 +4,3 @@ namespace:
   gcp-filestore-csi-driver
 resources:
 - node.yaml
-- psp.yaml
diff --git a/deploy/kubernetes/base/node_linux/psp.yaml b/deploy/kubernetes/base/node_linux/psp.yaml
deleted file mode 100644
index 6b6c262c..00000000
--- a/deploy/kubernetes/base/node_linux/psp.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-apiVersion: policy/v1beta1
-kind: PodSecurityPolicy
-metadata:
-  name: gcp-filestore-csi-node-psp
-spec:
-  seLinux:
-    rule: RunAsAny
-  supplementalGroups:
-    rule: RunAsAny
-  runAsUser:
-    rule: RunAsAny
-  fsGroup:
-    rule: RunAsAny
-  privileged: true
-  volumes:
-  - '*'
-  hostNetwork: true
-  allowedHostPaths:
-  - pathPrefix: "/var/lib/kubelet/plugins_registry/"
-  - pathPrefix: "/var/lib/kubelet"
-  - pathPrefix: "/var/lib/kubelet/plugins/filestore.csi.storage.gke.io/"
-
diff --git a/deploy/kubernetes/images/stable-master/image.yaml b/deploy/kubernetes/images/stable-master/image.yaml
index 97a6e02e..0dadad03 100644
--- a/deploy/kubernetes/images/stable-master/image.yaml
+++ b/deploy/kubernetes/images/stable-master/image.yaml
@@ -4,7 +4,7 @@ metadata:
   name: imagetag-csi-provisioner-prow
 imageTag:
   name: k8s.gcr.io/sig-storage/csi-provisioner
-  newTag: "v2.2.2"
+  newTag: "v3.1.0"
 ---
 
 apiVersion: builtin
diff --git a/pkg/cloud_provider/file/fake.go b/pkg/cloud_provider/file/fake.go
index e8d73475..d66aa57a 100644
--- a/pkg/cloud_provider/file/fake.go
+++ b/pkg/cloud_provider/file/fake.go
@@ -342,7 +342,17 @@ func (manager *fakeServiceManager) StartDeleteMultishareInstanceOp(ctx context.C
 }
 
 func (manager *fakeServiceManager) StartResizeMultishareInstanceOp(ctx context.Context, obj *MultishareInstance) (*filev1beta1multishare.Operation, error) {
-	return nil, nil
+	manager.createdMultishareInstance[obj.Name].CapacityBytes = obj.CapacityBytes
+	meta := &filev1beta1multishare.OperationMetadata{
+		Target: fmt.Sprintf(instanceURIFmt, obj.Project, obj.Location, obj.Name),
+		Verb:   "update",
+	}
+	metaBytes, _ := json.Marshal(meta)
+	op := &filev1beta1multishare.Operation{
+		Name:     "operation-" + uuid.New().String(),
+		Metadata: metaBytes,
+	}
+	return op, nil
 }
 
 func (manager *fakeServiceManager) StartCreateShareOp(ctx context.Context, obj *Share) (*filev1beta1multishare.Operation, error) {
@@ -404,7 +414,18 @@ func (manager *fakeServiceManager) StartDeleteShareOp(ctx context.Context, obj *
 }
 
 func (manager *fakeServiceManager) StartResizeShareOp(ctx context.Context, obj *Share) (*filev1beta1multishare.Operation, error) {
-	return nil, nil
+	manager.createdMultishares[obj.Name].CapacityBytes = obj.CapacityBytes
+	meta := &filev1beta1multishare.OperationMetadata{
+		Target: fmt.Sprintf(shareURIFmt, obj.Parent.Project, obj.Parent.Location, obj.Parent.Name, obj.Name),
+		Verb:   "update",
+	}
+	metaBytes, _ := json.Marshal(meta)
+	op := &filev1beta1multishare.Operation{
+		Name:     "operation-" + uuid.New().String(),
+		Metadata: metaBytes,
+	}
+
+	return op, nil
 }
 
 func (manager *fakeServiceManager) WaitForOpWithOpts(ctx context.Context, op string, opts PollOpts) error {
diff --git a/pkg/cloud_provider/file/file.go b/pkg/cloud_provider/file/file.go
index c33bdb6a..1b6c3e1b 100644
--- a/pkg/cloud_provider/file/file.go
+++ b/pkg/cloud_provider/file/file.go
@@ -153,8 +153,8 @@ const (
 	backupURIFmt    = locationURIFmt + "/backups/%s"
 	shareURIFmt     = instanceURIFmt + "/shares/%s"
 	// Patch update masks
-	fileShareUpdateMask                  = "file_shares"
-	multishareInstanceCapacityUpdateMask = "capacity_gb"
+	fileShareUpdateMask          = "file_shares"
+	multishareCapacityUpdateMask = "capacity_gb"
 )
 
 var _ Service = &gcfsServiceManager{}
@@ -340,7 +340,7 @@ func cloudInstanceToServiceInstance(instance *filev1beta1.Instance) (*ServiceIns
 
 func CompareInstances(a, b *ServiceInstance) error {
 	mismatches := []string{}
-	if strings.ToLower(a.Tier) != strings.ToLower(b.Tier) {
+	if !strings.EqualFold(a.Tier, b.Tier) {
 		mismatches = append(mismatches, "tier")
 	}
 	if a.Volume.Name != b.Volume.Name {
@@ -451,7 +451,7 @@ func (manager *gcfsServiceManager) ResizeInstance(ctx context.Context, obj *Serv
 	)
 	op, err := manager.instancesService.Patch(instanceuri, betaObj).UpdateMask(fileShareUpdateMask).Context(ctx).Do()
 	if err != nil {
-		return nil, fmt.Errorf("Patch operation failed: %v", err)
+		return nil, fmt.Errorf("patch operation failed: %v", err)
 	}
 
 	glog.V(4).Infof("For instance %s, waiting for patch op %v to complete", instanceuri, op.Name)
@@ -508,7 +508,7 @@ func (manager *gcfsServiceManager) CreateBackup(ctx context.Context, obj *Servic
 		return nil, err
 	}
 	if backupObj.State != "READY" {
-		return nil, fmt.Errorf("Backup %v for source %v is not ready, current state: %v", backupUri, backupSource, backupObj.State)
+		return nil, fmt.Errorf("backup %v for source %v is not ready, current state: %v", backupUri, backupSource, backupObj.State)
 	}
 	glog.Infof("Successfully created backup %+v for source instance %v", backupObj, backupSource)
 	return backupObj, nil
@@ -517,13 +517,13 @@ func (manager *gcfsServiceManager) CreateBackup(ctx context.Context, obj *Servic
 func (manager *gcfsServiceManager) DeleteBackup(ctx context.Context, backupId string) error {
 	opbackup, err := manager.backupService.Delete(backupId).Context(ctx).Do()
 	if err != nil {
-		return fmt.Errorf("For backup Id %s, delete backup operation %s failed: %v", backupId, opbackup.Name, err)
+		return fmt.Errorf("for backup Id %s, delete backup operation %s failed: %v", backupId, opbackup.Name, err)
 	}
 
 	glog.V(4).Infof("For backup Id %s, waiting for backup op %v to complete", backupId, opbackup.Name)
 	err = manager.waitForOp(ctx, opbackup)
 	if err != nil {
-		return fmt.Errorf("Delete backup: %v, op %s failed: %v", backupId, opbackup.Name, err)
+		return fmt.Errorf("delete backup: %v, op %s failed: %v", backupId, opbackup.Name, err)
 	}
 
 	glog.Infof("Backup %v successfully deleted", backupId)
@@ -646,7 +646,7 @@ func (manager *gcfsServiceManager) HasOperations(ctx context.Context, obj *Servi
 	for {
 		resp, err := manager.operationsService.List(locationURI(obj.Project, obj.Location)).PageToken(nextToken).Context(ctx).Do()
 		if err != nil {
-			return false, fmt.Errorf("List operations for instance %q, token %q failed: %v", uri, nextToken, err)
+			return false, fmt.Errorf("list operations for instance %q, token %q failed: %v", uri, nextToken, err)
 		}
 
 		filteredOps, err := ApplyFilter(resp.Operations, uri, operationType, done)
@@ -695,7 +695,7 @@ func (manager *gcfsServiceManager) GetMultishareInstance(ctx context.Context, ob
 }
 
 func (manager *gcfsServiceManager) ListMultishareInstances(ctx context.Context, filter *ListFilter) ([]*MultishareInstance, error) {
-	lCall := manager.multishareInstancesService.List(locationURI(filter.Project, "-")).Context(ctx)
+	lCall := manager.multishareInstancesService.List(locationURI(filter.Project, filter.Location)).Context(ctx)
 	nextPageToken := "pageToken"
 	var activeInstances []*MultishareInstance
 
@@ -777,9 +777,9 @@ func (manager *gcfsServiceManager) StartResizeMultishareInstanceOp(ctx context.C
 		Labels:      obj.Labels,
 		Description: obj.Description,
 	}
-	op, err := manager.multishareInstancesService.Patch(instanceuri, targetinstance).UpdateMask(multishareInstanceCapacityUpdateMask).Context(ctx).Do()
+	op, err := manager.multishareInstancesService.Patch(instanceuri, targetinstance).UpdateMask(multishareCapacityUpdateMask).Context(ctx).Do()
 	if err != nil {
-		return nil, fmt.Errorf("Patch operation failed: %v for instance %+v", err, targetinstance)
+		return nil, fmt.Errorf("patch operation failed: %v for instance %+v", err, targetinstance)
 	}
 
 	klog.Infof("Started instance update operation %s for instance %+v", op.Name, targetinstance)
@@ -812,9 +812,19 @@ func (manager *gcfsServiceManager) StartDeleteShareOp(ctx context.Context, share
 	return op, nil
 }
 
-func (manager *gcfsServiceManager) StartResizeShareOp(ctx context.Context, obj *Share) (*filev1beta1multishare.Operation, error) {
-	// TODO
-	return nil, nil
+func (manager *gcfsServiceManager) StartResizeShareOp(ctx context.Context, share *Share) (*filev1beta1multishare.Operation, error) {
+	uri := shareURI(share.Parent.Project, share.Parent.Location, share.Parent.Name, share.Name)
+	targetShare := &filev1beta1multishare.Share{
+		CapacityGb: util.BytesToGb(share.CapacityBytes),
+		Labels:     share.Labels,
+		MountName:  share.MountPointName,
+	}
+	op, err := manager.multishareInstancesSharesService.Patch(uri, targetShare).UpdateMask(multishareCapacityUpdateMask).Context(ctx).Do()
+	if err != nil {
+		return nil, fmt.Errorf("ResizeShare operation failed: %v", err)
+	}
+	klog.Infof("Started Resize Share op %s for share uri %q ", op.Name, uri)
+	return op, nil
 }
 
 func (manager *gcfsServiceManager) WaitForOpWithOpts(ctx context.Context, op string, opts PollOpts) error {
@@ -856,54 +866,71 @@ func (manager *gcfsServiceManager) GetShare(ctx context.Context, obj *Share) (*S
 		MountPointName: sobj.MountName,
 		CapacityBytes:  sobj.CapacityGb * util.Gb,
 		State:          sobj.State,
+		Labels:         sobj.Labels,
 	}, nil
 }
 
 func (manager *gcfsServiceManager) ListShares(ctx context.Context, filter *ListFilter) ([]*Share, error) {
-	uri := instanceURI(filter.Project, filter.Location, filter.InstanceName)
-	lCall := manager.multishareInstancesSharesService.List(uri).Context(ctx)
-	nextPageToken := "pageToken"
-	var shares []*Share
-
-	for nextPageToken != "" {
-		resp, err := lCall.Do()
+	instances := make([]*MultishareInstance, 0, 1)
+	if filter.InstanceName != "-" {
+		instances = append(instances, &MultishareInstance{Name: filter.InstanceName, Location: filter.Location})
+	} else {
+		existingInstances, err := manager.ListMultishareInstances(ctx, filter)
 		if err != nil {
-			klog.Errorf("list share error: %v", err)
 			return nil, err
 		}
+		instances = append(instances, existingInstances...)
+	}
+
+	var shares []*Share
 
-		for _, sobj := range resp.Shares {
-			project, location, instanceName, shareName, err := util.ParseShareURI(sobj.Name)
+	for _, instance := range instances {
+		uri := instanceURI(filter.Project, instance.Location, instance.Name)
+		lCall := manager.multishareInstancesSharesService.List(uri).Context(ctx)
+		nextPageToken := "pageToken"
+
+		for nextPageToken != "" {
+			resp, err := lCall.Do()
 			if err != nil {
-				klog.Errorf("Failed to parse share url :%s", sobj.Name)
-				continue
+				klog.Errorf("list share error: %v", err)
+				return nil, err
 			}
 
-			s := &Share{
-				Name: shareName,
-				Parent: &MultishareInstance{
-					Name:     instanceName,
-					Project:  project,
-					Location: location,
-				},
-				MountPointName: sobj.MountName,
-				CapacityBytes:  sobj.CapacityGb * util.Gb,
+			for _, sobj := range resp.Shares {
+				project, location, instanceName, shareName, err := util.ParseShareURI(sobj.Name)
+				if err != nil {
+					klog.Errorf("Failed to parse share url :%s", sobj.Name)
+					continue
+				}
+
+				s := &Share{
+					Name: shareName,
+					Parent: &MultishareInstance{
+						Name:     instanceName,
+						Project:  project,
+						Location: location,
+					},
+					MountPointName: sobj.MountName,
+					CapacityBytes:  sobj.CapacityGb * util.Gb,
+					Labels:         sobj.Labels,
+					State:          sobj.State,
+				}
+				shares = append(shares, s)
 			}
-			shares = append(shares, s)
+			nextPageToken = resp.NextPageToken
+			lCall.PageToken(nextPageToken)
 		}
-		nextPageToken = resp.NextPageToken
-		lCall.PageToken(nextPageToken)
 	}
 	return shares, nil
 }
 
 func ParseShare(s *Share) (string, string, string, string, error) {
 	if s == nil || s.Parent == nil {
-		return "", "", "", "", fmt.Errorf("Missing parent object for share %s", s.Name)
+		return "", "", "", "", fmt.Errorf("missing parent object for share %s", s.Name)
 	}
 
 	if s.Parent.Project == "" || s.Parent.Location == "" || s.Parent.Name == "" || s.Name == "" {
-		return "", "", "", "", fmt.Errorf("Missing identifier in share")
+		return "", "", "", "", fmt.Errorf("missing identifier in share")
 	}
 
 	return s.Parent.Project, s.Parent.Location, s.Parent.Name, s.Name, nil
@@ -934,7 +961,7 @@ func CompareMultishareInstances(a, b *MultishareInstance) error {
 		mismatches = append(mismatches, "name")
 	}
 
-	if strings.ToLower(a.Tier) != strings.ToLower(b.Tier) {
+	if !strings.EqualFold(a.Tier, b.Tier) {
 		mismatches = append(mismatches, "tier")
 	}
 
@@ -1040,11 +1067,11 @@ func IsShareTarget(target string) bool {
 
 func GenerateMultishareInstanceURI(m *MultishareInstance) (string, error) {
 	if m == nil {
-		return "", fmt.Errorf("Null instance")
+		return "", fmt.Errorf("nil instance")
 	}
 
 	if m.Project == "" || m.Location == "" || m.Name == "" {
-		return "", fmt.Errorf("Missing parent, project or location in instance")
+		return "", fmt.Errorf("missing parent, project or location in instance")
 	}
 
 	return fmt.Sprintf(instanceURIFmt, m.Project, m.Location, m.Name), nil
@@ -1052,11 +1079,11 @@ func GenerateMultishareInstanceURI(m *MultishareInstance) (string, error) {
 
 func GenerateShareURI(s *Share) (string, error) {
 	if s == nil || s.Parent == nil {
-		return "", fmt.Errorf("Missing share parent instance")
+		return "", fmt.Errorf("missing share parent instance")
 	}
 
 	if s.Parent.Project == "" || s.Parent.Location == "" || s.Parent.Name == "" || s.Name == "" {
-		return "", fmt.Errorf("Missing parent, project or location in share parent")
+		return "", fmt.Errorf("missing parent, project or location in share parent")
 	}
 
 	return fmt.Sprintf(shareURIFmt, s.Parent.Project, s.Parent.Location, s.Parent.Name, s.Name), nil
diff --git a/pkg/csi_driver/controller.go b/pkg/csi_driver/controller.go
index 973b89d9..000a3473 100644
--- a/pkg/csi_driver/controller.go
+++ b/pkg/csi_driver/controller.go
@@ -523,6 +523,18 @@ func (s *controllerServer) ControllerExpandVolume(ctx context.Context, req *csi.
 		return nil, status.Error(codes.InvalidArgument, "ControllerExpandVolume volume ID must be provided")
 	}
 
+	if isMultishareVolId(volumeID) {
+		if s.config.multiShareController == nil {
+			return nil, status.Error(codes.InvalidArgument, "multishare controller not enabled")
+		}
+		start := time.Now()
+		response, err := s.config.multiShareController.ControllerExpandVolume(ctx, req)
+		duration := time.Since(start)
+		s.config.metricsManager.RecordOperationMetrics(err, methodExpandVolume, modeMultishare, duration)
+		klog.Infof("ControllerExpandVolume response %+v error %v, for request: %+v", response, err, req)
+		return response, err
+	}
+
 	reqBytes, err := getRequestCapacity(req.GetCapacityRange())
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
diff --git a/pkg/csi_driver/gcfs_driver.go b/pkg/csi_driver/gcfs_driver.go
index 70707253..039d24c1 100644
--- a/pkg/csi_driver/gcfs_driver.go
+++ b/pkg/csi_driver/gcfs_driver.go
@@ -88,6 +88,7 @@ func NewGCFSDriver(config *GCFSDriverConfig) (*GCFSDriver, error) {
 	if config.RunNode {
 		nscap := []csi.NodeServiceCapability_RPC_Type{
 			csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
+			csi.NodeServiceCapability_RPC_GET_VOLUME_STATS,
 		}
 		driver.ns = newNodeServer(driver, config.Mounter, config.MetadataService)
 		driver.addNodeServiceCapabilities(nscap)
diff --git a/pkg/csi_driver/multishare_controller.go b/pkg/csi_driver/multishare_controller.go
index 3798b591..24e8402f 100644
--- a/pkg/csi_driver/multishare_controller.go
+++ b/pkg/csi_driver/multishare_controller.go
@@ -36,6 +36,7 @@ const (
 
 	methodCreateVolume         = "CreateVolume"
 	methodDeleteVolume         = "DeleteVolume"
+	methodExpandVolume         = "ExpandVolume"
 	ecfsDataPlaneVersionFormat = "GoogleReserved-CustomVMImage=clh.image.ems.path:projects/%s/global/images/ems-filestore-scaleout-%s"
 )
 
@@ -92,22 +93,18 @@ func (m *MultishareController) CreateVolume(ctx context.Context, req *csi.Create
 	}
 
 	if share != nil {
-		return m.getShareAndGenerateCSIResponse(ctx, instanceScPrefix, share)
+		return m.getShareAndGenerateCSICreateVolumeResponse(ctx, instanceScPrefix, share)
 	}
 
-	timeout, pollInterval, err := util.GetMultishareOpsTimeoutConfig(workflow.opType)
-	if err != nil {
-		return nil, status.Errorf(codes.Internal, err.Error())
-	}
 	// lock released. poll for op.
-	err = m.cloud.File.WaitForOpWithOpts(ctx, workflow.opName, file.PollOpts{Timeout: timeout, Interval: pollInterval})
+	err = m.waitOnWorkflow(ctx, workflow)
 	if err != nil {
 		return nil, status.Errorf(codes.Internal, "Create Volume failed, operation %q poll error: %v", workflow.opName, err)
 	}
 
 	klog.Infof("Poll for operation %s (type %s) completed", workflow.opName, workflow.opType.String())
 	if workflow.opType == util.ShareCreate {
-		return m.getShareAndGenerateCSIResponse(ctx, instanceScPrefix, workflow.share)
+		return m.getShareAndGenerateCSICreateVolumeResponse(ctx, instanceScPrefix, workflow.share)
 	}
 
 	var shareCreateWorkflow *Workflow
@@ -118,7 +115,7 @@ func (m *MultishareController) CreateVolume(ctx context.Context, req *csi.Create
 		if err != nil {
 			return nil, err
 		}
-		shareCreateWorkflow, err = m.opsManager.startShareCreateWorkflowSafe(ctx, instanceScPrefix, newShare)
+		shareCreateWorkflow, err = m.opsManager.startShareCreateWorkflowSafe(ctx, newShare)
 		if err != nil {
 			return nil, err
 		}
@@ -127,18 +124,14 @@ func (m *MultishareController) CreateVolume(ctx context.Context, req *csi.Create
 	}
 
 	// lock released. poll for share create op.
-	shareCreatetimeout, shareCreatePollInterval, err := util.GetMultishareOpsTimeoutConfig(shareCreateWorkflow.opType)
+	err = m.waitOnWorkflow(ctx, shareCreateWorkflow)
 	if err != nil {
-		return nil, status.Errorf(codes.Internal, err.Error())
-	}
-	err = m.cloud.File.WaitForOpWithOpts(ctx, shareCreateWorkflow.opName, file.PollOpts{Timeout: shareCreatetimeout, Interval: shareCreatePollInterval})
-	if err != nil {
-		return nil, status.Errorf(codes.Internal, "operation %q poll error: %v", workflow.opName, err)
+		return nil, status.Errorf(codes.Internal, "%s operation %q poll error: %v", shareCreateWorkflow.opType.String(), shareCreateWorkflow.opName, err)
 	}
-	return m.getShareAndGenerateCSIResponse(ctx, instanceScPrefix, newShare)
+	return m.getShareAndGenerateCSICreateVolumeResponse(ctx, instanceScPrefix, newShare)
 }
 
-func (m *MultishareController) getShareAndGenerateCSIResponse(ctx context.Context, instancePrefix string, s *file.Share) (*csi.CreateVolumeResponse, error) {
+func (m *MultishareController) getShareAndGenerateCSICreateVolumeResponse(ctx context.Context, instancePrefix string, s *file.Share) (*csi.CreateVolumeResponse, error) {
 	share, err := m.cloud.File.GetShare(ctx, s)
 	if err != nil {
 		return nil, status.Errorf(codes.Internal, err.Error())
@@ -151,7 +144,7 @@ func (m *MultishareController) getShareAndGenerateCSIResponse(ctx context.Contex
 }
 
 func (m *MultishareController) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
-	prefix, project, location, instanceName, shareName, err := parseMultishareVolId(req.VolumeId)
+	_, project, location, instanceName, shareName, err := parseMultishareVolId(req.VolumeId)
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
@@ -183,20 +176,16 @@ func (m *MultishareController) DeleteVolume(ctx context.Context, req *csi.Delete
 		return nil, status.Error(codes.Internal, err.Error())
 	}
 
-	workflow, err := m.opsManager.checkAndStartShareDeleteWorkflow(ctx, prefix, share)
+	workflow, err := m.opsManager.checkAndStartShareDeleteWorkflow(ctx, share)
 	if err != nil {
 		return nil, err
 	}
 
 	// Poll for share delete to complete
 	if workflow != nil {
-		shareDeletetimeout, shareDeletePollInterval, err := util.GetMultishareOpsTimeoutConfig(workflow.opType)
-		if err != nil {
-			return nil, status.Errorf(codes.Internal, err.Error())
-		}
-		err = m.cloud.File.WaitForOpWithOpts(ctx, workflow.opName, file.PollOpts{Timeout: shareDeletetimeout, Interval: shareDeletePollInterval})
+		err = m.waitOnWorkflow(ctx, workflow)
 		if err != nil {
-			return nil, status.Errorf(codes.Internal, "operation %q poll error: %v", workflow.opName, err)
+			return nil, status.Errorf(codes.Internal, "%s operation %q poll error: %v", workflow.opType.String(), workflow.opName, err)
 		}
 	}
 
@@ -228,30 +217,121 @@ func (m *MultishareController) startAndWaitForInstanceDeleteOrShrink(ctx context
 	if workflow == nil {
 		return nil
 	}
-	instanceOpTimeout, instanceOpPollInterval, err := util.GetMultishareOpsTimeoutConfig(workflow.opType)
-	if err != nil {
-		return status.Errorf(codes.Internal, err.Error())
-	}
-	err = m.cloud.File.WaitForOpWithOpts(ctx, workflow.opName, file.PollOpts{Timeout: instanceOpTimeout, Interval: instanceOpPollInterval})
+	err = m.waitOnWorkflow(ctx, workflow)
 	if err != nil {
-		return status.Errorf(codes.Internal, "operation %q poll error: %v", workflow.opName, err)
+		return status.Errorf(codes.Internal, "%s operation %q poll error: %v", workflow.opType.String(), workflow.opName, err)
 	}
 	return nil
 }
 
-func (m *MultishareController) ControllerExpandVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
+func (m *MultishareController) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
 	// Handle higher level csi params validation, try locks
 	// Initiate share workflow by calling Multishare OpsManager functions
 	// Prepare and return csi response
-	// TODO
-	return nil, status.Error(codes.Unimplemented, "ControllerExpandVolume unsupported for multi share instances")
+
+	volumeId := req.GetVolumeId()
+	if len(volumeId) == 0 {
+		return nil, status.Error(codes.InvalidArgument, "ControllerExpandVolume volume ID must be provided")
+	}
+
+	reqBytes, err := getShareRequestCapacity(req.GetCapacityRange())
+	if err != nil {
+		return nil, status.Error(codes.InvalidArgument, err.Error())
+	}
+
+	_, project, location, instanceName, shareName, err := parseMultishareVolId(req.VolumeId)
+	if err != nil {
+		return nil, status.Error(codes.InvalidArgument, err.Error())
+	}
+
+	klog.Infof("ControllerExpandVolume called for multishare with request %+v", req)
+	if acquired := m.volumeLocks.TryAcquire(volumeId); !acquired {
+		return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeId)
+	}
+	defer m.volumeLocks.Release(volumeId)
+
+	share, err := m.cloud.File.GetShare(ctx, &file.Share{
+		Parent: &file.MultishareInstance{
+			Project:  project,
+			Location: location,
+			Name:     instanceName,
+		},
+		Name: shareName,
+	})
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, err.Error())
+	}
+	if share == nil || file.IsNotFoundErr(err) {
+		return nil, status.Errorf(codes.Internal, "Couldn't find share with name %q", volumeId)
+	}
+
+	if share.CapacityBytes >= reqBytes {
+		klog.Infof("Controller expand volume succeeded for volume %v, existing size(bytes): %v", volumeId, share.CapacityBytes)
+		return &csi.ControllerExpandVolumeResponse{
+			CapacityBytes:         share.CapacityBytes,
+			NodeExpansionRequired: false,
+		}, nil
+	}
+
+	workflow, err := m.opsManager.checkAndStartInstanceOrShareExpandWorkflow(ctx, share, reqBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	err = m.waitOnWorkflow(ctx, workflow)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "wait on %s operation %q failed with error: %v", workflow.opType.String(), workflow.opName, err)
+	}
+	klog.Infof("Wait for operation %s (type %s) completed", workflow.opName, workflow.opType.String())
+
+	switch workflow.opType {
+	case util.InstanceUpdate:
+		workflow, err = m.opsManager.startShareExpandWorkflowSafe(ctx, share, reqBytes)
+		if err != nil {
+			return nil, err
+		}
+	case util.ShareUpdate:
+		return m.getShareAndGenerateCSIControllerExpandVolumeResponse(ctx, share, reqBytes)
+	default:
+		return nil, status.Errorf(codes.Internal, "Controller Expand Volume failed, unknown workflow %v detected", workflow.opType)
+	}
+
+	err = m.waitOnWorkflow(ctx, workflow)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "wait on share expansion op %q failed with error: %v", workflow.opName, err)
+	}
+
+	return m.getShareAndGenerateCSIControllerExpandVolumeResponse(ctx, share, reqBytes)
+}
+
+func (m *MultishareController) getShareAndGenerateCSIControllerExpandVolumeResponse(ctx context.Context, share *file.Share, reqBytes int64) (*csi.ControllerExpandVolumeResponse, error) {
+	share, err := m.cloud.File.GetShare(ctx, share)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, err.Error())
+	}
+	if share.CapacityBytes < reqBytes {
+		return nil, status.Errorf(codes.Aborted, "expand volume operation succeeded but share capacity [%d]bytes smaller than requested [%d]bytes", share.CapacityBytes, reqBytes)
+	}
+	return &csi.ControllerExpandVolumeResponse{
+		CapacityBytes:         share.CapacityBytes,
+		NodeExpansionRequired: false,
+	}, nil
+}
+
+func (m *MultishareController) waitOnWorkflow(ctx context.Context, workflow *Workflow) (err error) {
+	timeout, pollInterval, err := util.GetMultishareOpsTimeoutConfig(workflow.opType)
+	if err != nil {
+		return
+	}
+	err = m.cloud.File.WaitForOpWithOpts(ctx, workflow.opName, file.PollOpts{Timeout: timeout, Interval: pollInterval})
+	return
 }
 
 func getInstanceSCPrefix(req *csi.CreateVolumeRequest) (string, error) {
 	params := req.GetParameters()
 	v, ok := params[paramMultishareInstanceScLabel]
 	if !ok {
-		return "", fmt.Errorf("Failed to find instance prefix key")
+		return "", fmt.Errorf("failed to find instance prefix key")
 	}
 
 	if v == "" {
diff --git a/pkg/csi_driver/multishare_ops_manager.go b/pkg/csi_driver/multishare_ops_manager.go
index 3801603e..143702ff 100644
--- a/pkg/csi_driver/multishare_ops_manager.go
+++ b/pkg/csi_driver/multishare_ops_manager.go
@@ -109,7 +109,7 @@ func (m *MultishareOpsManager) setupEligibleInstanceAndStartWorkflow(ctx context
 			return nil, nil, status.Error(codes.Internal, err.Error())
 		}
 
-		needExpand, targetBytes, err := m.instanceNeedsExpand(ctx, share)
+		needExpand, targetBytes, err := m.instanceNeedsExpand(ctx, share, share.CapacityBytes)
 		if err != nil {
 			return nil, nil, status.Error(codes.Internal, err.Error())
 		}
@@ -133,7 +133,7 @@ func (m *MultishareOpsManager) setupEligibleInstanceAndStartWorkflow(ctx context
 	return w, nil, err
 }
 
-func (m *MultishareOpsManager) startShareCreateWorkflowSafe(ctx context.Context, instanceSCPrefix string, share *file.Share) (*Workflow, error) {
+func (m *MultishareOpsManager) startShareCreateWorkflowSafe(ctx context.Context, share *file.Share) (*Workflow, error) {
 	m.Lock()
 	defer m.Unlock()
 	ops, err := m.listMultishareResourceRunningOps(ctx)
@@ -319,12 +319,12 @@ func (m *MultishareOpsManager) runEligibleInstanceCheck(ctx context.Context, ins
 	return readyEligibleInstances, nonReadyInstanceCount, nil
 }
 
-func (m *MultishareOpsManager) instanceNeedsExpand(ctx context.Context, share *file.Share) (bool, int64, error) {
+func (m *MultishareOpsManager) instanceNeedsExpand(ctx context.Context, share *file.Share, capacityNeeded int64) (bool, int64, error) {
 	if share == nil {
 		return false, 0, fmt.Errorf("empty share")
 	}
 	if share.Parent == nil {
-		return false, 0, fmt.Errorf("Parent missing from share %q", share.Name)
+		return false, 0, fmt.Errorf("parent missing from share %q", share.Name)
 	}
 
 	shares, err := m.cloud.File.ListShares(ctx, &file.ListFilter{Project: share.Parent.Project, Location: share.Parent.Location, InstanceName: share.Parent.Name})
@@ -336,16 +336,72 @@ func (m *MultishareOpsManager) instanceNeedsExpand(ctx context.Context, share *f
 	for _, s := range shares {
 		sumShareBytes = sumShareBytes + s.CapacityBytes
 	}
-	// TODO: Check if we need to align the increment to step size.
-	var remainingBytes int64
-	remainingBytes = share.Parent.CapacityBytes - sumShareBytes
-	if remainingBytes < share.CapacityBytes {
-		return true, (share.CapacityBytes + sumShareBytes), nil
+
+	remainingBytes := share.Parent.CapacityBytes - sumShareBytes
+	if remainingBytes < capacityNeeded {
+		alignBytes := util.AlignBytes(capacityNeeded+sumShareBytes, util.GbToBytes(share.Parent.CapacityStepSizeGb))
+		targetBytes := util.Min(alignBytes, util.MaxMultishareInstanceSizeBytes)
+		return true, targetBytes, nil
 	}
 	return false, 0, nil
 }
 
-func (m *MultishareOpsManager) checkAndStartShareDeleteWorkflow(ctx context.Context, instanceScPrefix string, share *file.Share) (*Workflow, error) {
+func (m *MultishareOpsManager) checkAndStartInstanceOrShareExpandWorkflow(ctx context.Context, share *file.Share, reqBytes int64) (*Workflow, error) {
+	m.Lock()
+	defer m.Unlock()
+
+	ops, err := m.listMultishareResourceRunningOps(ctx)
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	expandShareOp, err := containsOpWithShareTarget(share, util.ShareUpdate, ops)
+	if err != nil {
+		return nil, err
+	}
+	if expandShareOp != nil {
+		return &Workflow{share: share, opName: expandShareOp.Id, opType: expandShareOp.Type}, nil
+	}
+
+	// no existing share Expansion, proceed to instance check
+	err = m.verifyNoRunningInstanceOrShareOpsForInstance(share.Parent, ops)
+	if err != nil {
+		klog.Infof("Instance %v has running share or instnace Op, aborting volume expansion.", share.Parent.Name)
+		return nil, status.Error(codes.Aborted, err.Error())
+	}
+
+	instance, err := m.cloud.File.GetMultishareInstance(ctx, share.Parent)
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	needExpand, targetBytes, err := m.instanceNeedsExpand(ctx, share, reqBytes-share.CapacityBytes)
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+	if needExpand {
+		instance.CapacityBytes = targetBytes
+		workflow, err := m.startInstanceWorkflow(ctx, &Workflow{instance: instance, opType: util.InstanceUpdate}, ops)
+		return workflow, err
+	}
+
+	share.CapacityBytes = reqBytes
+	return m.startShareWorkflow(ctx, &Workflow{share: share, opType: util.ShareUpdate}, ops)
+}
+
+func (m *MultishareOpsManager) startShareExpandWorkflowSafe(ctx context.Context, share *file.Share, reqBytes int64) (*Workflow, error) {
+	m.Lock()
+	defer m.Unlock()
+	ops, err := m.listMultishareResourceRunningOps(ctx)
+	if err != nil {
+		return nil, status.Error(codes.Internal, err.Error())
+	}
+
+	share.CapacityBytes = reqBytes
+	return m.startShareWorkflow(ctx, &Workflow{share: share, opType: util.ShareUpdate}, ops)
+}
+
+func (m *MultishareOpsManager) checkAndStartShareDeleteWorkflow(ctx context.Context, share *file.Share) (*Workflow, error) {
 	m.Lock()
 	defer m.Unlock()
 
@@ -355,12 +411,12 @@ func (m *MultishareOpsManager) checkAndStartShareDeleteWorkflow(ctx context.Cont
 	}
 
 	// If we find a running delete share op, poll for that to complete.
-	deleteshareop, err := containsOpWithShareTarget(share, util.ShareDelete, ops)
+	deleteShareOp, err := containsOpWithShareTarget(share, util.ShareDelete, ops)
 	if err != nil {
 		return nil, err
 	}
-	if deleteshareop != nil {
-		return &Workflow{share: share, opName: deleteshareop.Id, opType: deleteshareop.Type}, nil
+	if deleteShareOp != nil {
+		return &Workflow{share: share, opName: deleteShareOp.Id, opType: deleteShareOp.Type}, nil
 	}
 
 	return m.startShareWorkflow(ctx, &Workflow{share: share, opType: util.ShareDelete}, ops)
@@ -418,7 +474,13 @@ func (m *MultishareOpsManager) checkAndStartInstanceDeleteOrShrinkWorkflow(ctx c
 		totalShareCap += share.CapacityBytes
 	}
 	if totalShareCap < instance.CapacityBytes && instance.CapacityBytes > util.MinMultishareInstanceSizeBytes {
-		instance.CapacityBytes = util.Max(totalShareCap, util.MinMultishareInstanceSizeBytes)
+		targetShrinkSizeBytes := util.AlignBytes(totalShareCap, util.GbToBytes(instance.CapacityStepSizeGb))
+		targetShrinkSizeBytes = util.Max(targetShrinkSizeBytes, util.MinMultishareInstanceSizeBytes)
+		if instance.CapacityBytes == targetShrinkSizeBytes {
+			return nil, nil
+		}
+
+		instance.CapacityBytes = targetShrinkSizeBytes
 		w, err := m.startInstanceWorkflow(ctx, &Workflow{instance: instance, opType: util.InstanceUpdate}, ops)
 		if err != nil {
 			if file.IsNotFoundErr(err) {
diff --git a/pkg/csi_driver/node.go b/pkg/csi_driver/node.go
index 9be1a73e..b02813ab 100644
--- a/pkg/csi_driver/node.go
+++ b/pkg/csi_driver/node.go
@@ -25,6 +25,7 @@ import (
 	csi "github.com/container-storage-interface/spec/lib/go/csi"
 	"github.com/golang/glog"
 	"golang.org/x/net/context"
+	"golang.org/x/sys/unix"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	mount "k8s.io/mount-utils"
@@ -179,6 +180,46 @@ func (s *nodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCa
 	}, nil
 }
 
+func (s *nodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
+	if len(req.VolumeId) == 0 {
+		return nil, status.Error(codes.InvalidArgument, "NodeGetVolumeStats volume ID was empty")
+	}
+	if len(req.VolumePath) == 0 {
+		return nil, status.Error(codes.InvalidArgument, "NodeGetVolumeStats volume path was empty")
+	}
+
+	_, err := os.Lstat(req.VolumePath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, status.Errorf(codes.NotFound, "path %s does not exist", req.VolumePath)
+		}
+		return nil, status.Errorf(codes.Internal, "unknown error when stat on %s: %v", req.VolumePath, err)
+	}
+
+	available, capacity, used, inodesFree, inodes, inodesUsed, err := getFSStat(req.VolumePath)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "failed to get fs info on path %s: %v", req.VolumePath, err)
+	}
+
+	return &csi.NodeGetVolumeStatsResponse{
+		Usage: []*csi.VolumeUsage{
+			{
+				Unit:      csi.VolumeUsage_BYTES,
+				Available: available,
+				Total:     capacity,
+				Used:      used,
+			},
+			{
+				Unit:      csi.VolumeUsage_INODES,
+				Available: inodesFree,
+				Total:     inodes,
+				Used:      inodesUsed,
+			},
+		},
+	}, nil
+
+}
+
 func (s *nodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
 	// Validate Arguments
 	volumeID := req.GetVolumeId()
@@ -389,3 +430,23 @@ func validateMultishareVolumeAttributes(attr map[string]string) error {
 	}
 	return nil
 }
+
+func getFSStat(path string) (available, capacity, used, inodesFree, inodes, inodesUsed int64, err error) {
+	statfs := &unix.Statfs_t{}
+	err = unix.Statfs(path, statfs)
+	if err != nil {
+		err = fmt.Errorf("failed to get fs info on path %s: %v", path, err)
+		return
+	}
+
+	// Available is blocks available * fragment size to root user
+	available = int64(statfs.Bfree) * int64(statfs.Bsize)
+	// Capacity is total block count * fragment size
+	capacity = int64(statfs.Blocks) * int64(statfs.Bsize)
+	// Usage is block being used * fragment size (aka block size).
+	used = (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize)
+	inodes = int64(statfs.Files)
+	inodesFree = int64(statfs.Ffree)
+	inodesUsed = inodes - inodesFree
+	return
+}
diff --git a/pkg/csi_driver/node_unimpl.go b/pkg/csi_driver/node_unimpl.go
index 4fa56c7e..4fcf2240 100644
--- a/pkg/csi_driver/node_unimpl.go
+++ b/pkg/csi_driver/node_unimpl.go
@@ -26,7 +26,3 @@ import (
 func (s *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
 	return nil, status.Error(codes.Unimplemented, "NodeUnStageVolume unsupported")
 }
-
-func (s *nodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
-	return nil, status.Error(codes.Unimplemented, "NodeUnStageVolume unsupported")
-}
diff --git a/pkg/util/multishare_defs.go b/pkg/util/multishare_defs.go
index 66028a76..37dad795 100644
--- a/pkg/util/multishare_defs.go
+++ b/pkg/util/multishare_defs.go
@@ -22,6 +22,7 @@ const (
 	MultishareCSIVolIdSplitLen = 6
 
 	MinMultishareInstanceSizeBytes    int64 = 1 * Tb
+	MaxMultishareInstanceSizeBytes    int64 = 10 * Tb
 	MaxShareSizeBytes                 int64 = 1 * Tb
 	MinShareSizeBytes                 int64 = 100 * Gb
 	MaxSharesPerInstance                    = 10
diff --git a/pkg/util/util.go b/pkg/util/util.go
index 337be888..62f410e6 100644
--- a/pkg/util/util.go
+++ b/pkg/util/util.go
@@ -29,7 +29,8 @@ import (
 )
 
 const (
-	Gb = 1024 * 1024 * 1024
+	Mb = 1024 * 1024
+	Gb = 1024 * Mb
 	Tb = 1024 * Gb
 
 	// VolumeSnapshot parameters
@@ -62,6 +63,10 @@ func GbToBytes(gbs int64) int64 {
 	return gbs * Gb
 }
 
+func MbToBytes(mbs int64) int64 {
+	return mbs * Mb
+}
+
 func Min(a, b int64) int64 {
 	if a < b {
 		return a
@@ -264,3 +269,11 @@ func GetMultishareOpsTimeoutConfig(opType OperationType) (time.Duration, time.Du
 		return 0, 0, fmt.Errorf("unknown op type %v", opType)
 	}
 }
+
+// Aligns to the next higher multiple of step size. No-op if step size is 0, or already aligned.
+func AlignBytes(currBytes int64, stepBytes int64) int64 {
+	if stepBytes == 0 {
+		return currBytes
+	}
+	return ((currBytes + stepBytes - 1) / stepBytes) * stepBytes
+}
diff --git a/vendor/google.golang.org/api/file/v1beta1multishare/file-gen.go b/vendor/google.golang.org/api/file/v1beta1multishare/file-gen.go
index 9e595561..db832afc 100755
--- a/vendor/google.golang.org/api/file/v1beta1multishare/file-gen.go
+++ b/vendor/google.golang.org/api/file/v1beta1multishare/file-gen.go
@@ -518,11 +518,12 @@ type GoogleCloudSaasacceleratorManagementProvidersV1Instance struct {
 	CreateTime string `json:"createTime,omitempty"`
 
 	// InstanceType: Optional. The instance_type of this instance of format:
-	// projects/{project_id}/locations/{location_id}/instanceTypes/{instance_
-	// type_id}. Instance Type represents a high-level tier or SKU of the
-	// service that this instance belong to. When enabled(eg: Maintenance
-	// Rollout), Rollout uses 'instance_type' along with 'software_versions'
-	// to determine whether instance needs an update or not.
+	// projects/{project_number}/locations/{location_id}/instanceTypes/{insta
+	// nce_type_id}. Instance Type represents a high-level tier or SKU of
+	// the service that this instance belong to. When enabled(eg:
+	// Maintenance Rollout), Rollout uses 'instance_type' along with
+	// 'software_versions' to determine whether instance needs an update or
+	// not.
 	InstanceType string `json:"instanceType,omitempty"`
 
 	// Labels: Optional. Resource labels to represent user provided
@@ -547,9 +548,11 @@ type GoogleCloudSaasacceleratorManagementProvidersV1Instance struct {
 	MaintenanceSettings *GoogleCloudSaasacceleratorManagementProvidersV1MaintenanceSettings `json:"maintenanceSettings,omitempty"`
 
 	// Name: Unique name of the resource. It uses the form:
-	// `projects/{project_id|project_number}/locations/{location_id}/instance
-	// s/{instance_id}` Note: Either project_id or project_number can be
-	// used, but keep it consistent with other APIs (e.g. RescheduleUpdate)
+	// `projects/{project_number}/locations/{location_id}/instances/{instance
+	// _id}` Note: This name is passed, stored and logged across the rollout
+	// system. So use of consumer project_id or any other consumer PII in
+	// the name is strongly discouraged for wipeout (go/wipeout) compliance.
+	// See go/elysium/project_ids#storage-guidance for more details.
 	Name string `json:"name,omitempty"`
 
 	// NotificationParameters: Optional. notification_parameter are
@@ -1271,6 +1274,9 @@ type ListSharesResponse struct {
 	// Shares: A list of shares in the project for the specified instance.
 	Shares []*Share `json:"shares,omitempty"`
 
+	// Unreachable: Locations that could not be reached.
+	Unreachable []string `json:"unreachable,omitempty"`
+
 	// ServerResponse contains the HTTP response code and headers from the
 	// server.
 	googleapi.ServerResponse `json:"-"`
@@ -1889,7 +1895,7 @@ type Share struct {
 	// Possible values:
 	//   "STATE_UNSPECIFIED" - State not set.
 	//   "CREATING" - Share is being created.
-	//   "ACTIVE" - Share is available for use.
+	//   "READY" - Share is ready for use.
 	//   "DELETING" - Share is being deleted.
 	State string `json:"state,omitempty"`
 
@@ -2307,8 +2313,8 @@ func (r *ProjectsLocationsService) List(name string) *ProjectsLocationsListCall
 
 // Filter sets the optional parameter "filter": A filter to narrow down
 // results to a preferred subset. The filtering language accepts strings
-// like "displayName=tokyo", and is documented in more detail in AIP-160
-// (https://google.aip.dev/160).
+// like "displayName=tokyo", and is documented in more detail in
+// AIP-160 (https://google.aip.dev/160).
 func (c *ProjectsLocationsListCall) Filter(filter string) *ProjectsLocationsListCall {
 	c.urlParams_.Set("filter", filter)
 	return c
@@ -2445,7 +2451,7 @@ func (c *ProjectsLocationsListCall) Do(opts ...googleapi.CallOption) (*ListLocat
 	//   ],
 	//   "parameters": {
 	//     "filter": {
-	//       "description": "A filter to narrow down results to a preferred subset. The filtering language accepts strings like \"displayName=tokyo\", and is documented in more detail in [AIP-160](https://google.aip.dev/160).",
+	//       "description": "A filter to narrow down results to a preferred subset. The filtering language accepts strings like `\"displayName=tokyo\"`, and is documented in more detail in [AIP-160](https://google.aip.dev/160).",
 	//       "location": "query",
 	//       "type": "string"
 	//     },
@@ -5112,6 +5118,166 @@ func (c *ProjectsLocationsInstancesSharesListCall) Pages(ctx context.Context, f
 	}
 }
 
+// method id "file.projects.locations.instances.shares.patch":
+
+type ProjectsLocationsInstancesSharesPatchCall struct {
+	s          *Service
+	name       string
+	share      *Share
+	urlParams_ gensupport.URLParams
+	ctx_       context.Context
+	header_    http.Header
+}
+
+// Patch: Updates the settings of a specific share.
+//
+// - name: The resource name of the share, in the format
+//   `projects/{project_id}/locations/{location_id}/instances/{instance_i
+//   d}/shares/{share_id}`.
+func (r *ProjectsLocationsInstancesSharesService) Patch(name string, share *Share) *ProjectsLocationsInstancesSharesPatchCall {
+	c := &ProjectsLocationsInstancesSharesPatchCall{s: r.s, urlParams_: make(gensupport.URLParams)}
+	c.name = name
+	c.share = share
+	return c
+}
+
+// UpdateMask sets the optional parameter "updateMask": Required. Mask
+// of fields to update. At least one path must be supplied in this
+// field. The elements of the repeated paths field may only include
+// these fields: * "description" * "capacity_gb" * "labels" *
+// "nfs_export_options"
+func (c *ProjectsLocationsInstancesSharesPatchCall) UpdateMask(updateMask string) *ProjectsLocationsInstancesSharesPatchCall {
+	c.urlParams_.Set("updateMask", updateMask)
+	return c
+}
+
+// Fields allows partial responses to be retrieved. See
+// https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
+// for more information.
+func (c *ProjectsLocationsInstancesSharesPatchCall) Fields(s ...googleapi.Field) *ProjectsLocationsInstancesSharesPatchCall {
+	c.urlParams_.Set("fields", googleapi.CombineFields(s))
+	return c
+}
+
+// Context sets the context to be used in this call's Do method. Any
+// pending HTTP request will be aborted if the provided context is
+// canceled.
+func (c *ProjectsLocationsInstancesSharesPatchCall) Context(ctx context.Context) *ProjectsLocationsInstancesSharesPatchCall {
+	c.ctx_ = ctx
+	return c
+}
+
+// Header returns an http.Header that can be modified by the caller to
+// add HTTP headers to the request.
+func (c *ProjectsLocationsInstancesSharesPatchCall) Header() http.Header {
+	if c.header_ == nil {
+		c.header_ = make(http.Header)
+	}
+	return c.header_
+}
+
+func (c *ProjectsLocationsInstancesSharesPatchCall) doRequest(alt string) (*http.Response, error) {
+	reqHeaders := make(http.Header)
+	reqHeaders.Set("x-goog-api-client", "gl-go/"+gensupport.GoVersion()+" gdcl/20220111")
+	for k, v := range c.header_ {
+		reqHeaders[k] = v
+	}
+	reqHeaders.Set("User-Agent", c.s.userAgent())
+	var body io.Reader = nil
+	body, err := googleapi.WithoutDataWrapper.JSONReader(c.share)
+	if err != nil {
+		return nil, err
+	}
+	reqHeaders.Set("Content-Type", "application/json")
+	c.urlParams_.Set("alt", alt)
+	c.urlParams_.Set("prettyPrint", "false")
+	urls := googleapi.ResolveRelative(c.s.BasePath, "v1beta1/{+name}")
+	urls += "?" + c.urlParams_.Encode()
+	req, err := http.NewRequest("PATCH", urls, body)
+	if err != nil {
+		return nil, err
+	}
+	req.Header = reqHeaders
+	googleapi.Expand(req.URL, map[string]string{
+		"name": c.name,
+	})
+	return gensupport.SendRequest(c.ctx_, c.s.client, req)
+}
+
+// Do executes the "file.projects.locations.instances.shares.patch" call.
+// Exactly one of *Operation or error will be non-nil. Any non-2xx
+// status code is an error. Response headers are in either
+// *Operation.ServerResponse.Header or (if a response was returned at
+// all) in error.(*googleapi.Error).Header. Use googleapi.IsNotModified
+// to check whether the returned error was because
+// http.StatusNotModified was returned.
+func (c *ProjectsLocationsInstancesSharesPatchCall) Do(opts ...googleapi.CallOption) (*Operation, error) {
+	gensupport.SetOptions(c.urlParams_, opts...)
+	res, err := c.doRequest("json")
+	if res != nil && res.StatusCode == http.StatusNotModified {
+		if res.Body != nil {
+			res.Body.Close()
+		}
+		return nil, &googleapi.Error{
+			Code:   res.StatusCode,
+			Header: res.Header,
+		}
+	}
+	if err != nil {
+		return nil, err
+	}
+	defer googleapi.CloseBody(res)
+	if err := googleapi.CheckResponse(res); err != nil {
+		return nil, err
+	}
+	ret := &Operation{
+		ServerResponse: googleapi.ServerResponse{
+			Header:         res.Header,
+			HTTPStatusCode: res.StatusCode,
+		},
+	}
+	target := &ret
+	if err := gensupport.DecodeResponse(target, res); err != nil {
+		return nil, err
+	}
+	return ret, nil
+	// {
+	//   "description": "Updates the settings of a specific share.",
+	//   "flatPath": "v1beta1/projects/{projectsId}/locations/{locationsId}/instances/{instancesId}/shares/{sharesId}",
+	//   "httpMethod": "PATCH",
+	//   "id": "file.projects.locations.instances.shares.patch",
+	//   "parameterOrder": [
+	//     "name"
+	//   ],
+	//   "parameters": {
+	//     "name": {
+	//       "description": "Output only. The resource name of the share, in the format `projects/{project_id}/locations/{location_id}/instances/{instance_id}/shares/{share_id}`.",
+	//       "location": "path",
+	//       "pattern": "^projects/[^/]+/locations/[^/]+/instances/[^/]+/shares/[^/]+$",
+	//       "required": true,
+	//       "type": "string"
+	//     },
+	//     "updateMask": {
+	//       "description": "Required. Mask of fields to update. At least one path must be supplied in this field. The elements of the repeated paths field may only include these fields: * \"description\" * \"capacity_gb\" * \"labels\" * \"nfs_export_options\"",
+	//       "format": "google-fieldmask",
+	//       "location": "query",
+	//       "type": "string"
+	//     }
+	//   },
+	//   "path": "v1beta1/{+name}",
+	//   "request": {
+	//     "$ref": "Share"
+	//   },
+	//   "response": {
+	//     "$ref": "Operation"
+	//   },
+	//   "scopes": [
+	//     "https://www.googleapis.com/auth/cloud-platform"
+	//   ]
+	// }
+
+}
+
 // method id "file.projects.locations.instances.snapshots.create":
 
 type ProjectsLocationsInstancesSnapshotsCreateCall struct {


Test patch:
diff --git a/pkg/csi_driver/multishare_controller_test.go b/pkg/csi_driver/multishare_controller_test.go
index 44e0c99e..d22e594c 100644
--- a/pkg/csi_driver/multishare_controller_test.go
+++ b/pkg/csi_driver/multishare_controller_test.go
@@ -835,7 +835,7 @@ func TestMultishareCreateVolume(t *testing.T) {
 			if err != nil {
 				t.Fatalf("failed to fake service: %v", err)
 			}
-			cloudProvider, err := cloud.NewFakeCloud()
+			cloudProvider, _ := cloud.NewFakeCloud()
 			cloudProvider.File = s
 			mcs := NewMultishareController(initTestDriver(t), s, cloudProvider, util.NewVolumeLocks(), "")
 			resp, err := mcs.CreateVolume(context.Background(), tc.req)
@@ -1032,7 +1032,7 @@ func TestMultishareDeleteVolume(t *testing.T) {
 			if err != nil {
 				t.Fatalf("failed to fake service: %v", err)
 			}
-			cloudProvider, err := cloud.NewFakeCloud()
+			cloudProvider, _ := cloud.NewFakeCloud()
 			cloudProvider.File = s
 			mcs := NewMultishareController(initTestDriver(t), s, cloudProvider, util.NewVolumeLocks(), "")
 			resp, err := mcs.DeleteVolume(context.Background(), tc.req)
@@ -1055,3 +1055,384 @@ func TestMultishareDeleteVolume(t *testing.T) {
 	}
 
 }
+
+func TestMultishareControllerExpandVolume(t *testing.T) {
+	testVolName := "pvc-" + string(uuid.NewUUID())
+	testShareName := util.ConvertVolToShareName(testVolName)
+	testInstanceName1 := "fs-" + string(uuid.NewUUID())
+	testVolId := fmt.Sprintf("%s/%s/%s/%s/%s/%s", modeMultishare, testInstanceScPrefix, testProject, testRegion, testInstanceName1, testShareName)
+	baseCap := 100 * util.Gb
+	mediumCap := 200 * util.Gb
+	largeCap := 500 * util.Gb
+	type OpItem struct {
+		id     string
+		target string
+		verb   string
+		done   bool
+	}
+	tests := []struct {
+		name          string
+		ops           []OpItem
+		initInstance  []*file.MultishareInstance
+		initShares    []*file.Share
+		req           *csi.ControllerExpandVolumeRequest
+		resp          *csi.ControllerExpandVolumeResponse
+		errorExpected bool
+	}{
+		{
+			name: "share not found, error respond",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(mediumCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			errorExpected: true,
+		},
+		{
+			name: "share already larger than request, succeed",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(mediumCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			initShares: []*file.Share{
+				{
+					Name: testShareName,
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName,
+					CapacityBytes:  int64(largeCap),
+				},
+			},
+			resp: &csi.ControllerExpandVolumeResponse{
+				CapacityBytes:         int64(largeCap),
+				NodeExpansionRequired: false,
+			},
+		},
+		{
+			name: "share found, instance op ongoing, error",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(mediumCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			initShares: []*file.Share{
+				{
+					Name: testShareName,
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName,
+					CapacityBytes:  int64(baseCap),
+				},
+			},
+			ops: []OpItem{
+				{
+					id:     "op1",
+					target: fmt.Sprintf(instanceUriFmt, testProject, testRegion, testInstanceName1),
+					verb:   "update",
+				},
+			},
+			errorExpected: true,
+		},
+		{
+			name: "share found, instance needs expansion",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(largeCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			initShares: []*file.Share{
+				{
+					Name: testShareName,
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName,
+					CapacityBytes:  int64(baseCap),
+				},
+				{
+					Name: testShareName + "1",
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName + "1",
+					CapacityBytes:  int64(mediumCap),
+				},
+				{
+					Name: testShareName + "2",
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName + "2",
+					CapacityBytes:  int64(largeCap),
+				},
+			},
+			resp: &csi.ControllerExpandVolumeResponse{
+				CapacityBytes:         int64(largeCap),
+				NodeExpansionRequired: false,
+			},
+		},
+		{
+			name: "share found, instance does not need expansion",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(largeCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			initShares: []*file.Share{
+				{
+					Name: testShareName,
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName,
+					CapacityBytes:  int64(baseCap),
+				},
+			},
+			resp: &csi.ControllerExpandVolumeResponse{
+				CapacityBytes:         int64(largeCap),
+				NodeExpansionRequired: false,
+			},
+		},
+		{
+			name: "shareOp on other shares ongoing for instance",
+			req: &csi.ControllerExpandVolumeRequest{
+				VolumeId:      testVolId,
+				CapacityRange: &csi.CapacityRange{RequiredBytes: int64(mediumCap)},
+			},
+			initInstance: []*file.MultishareInstance{
+				{
+					Name:     testInstanceName1,
+					Location: testRegion,
+					Project:  testProject,
+					Labels: map[string]string{
+						util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+					},
+					CapacityBytes: 1 * util.Tb,
+					Tier:          "Enterprise",
+					Network: file.Network{
+						Ip: testIP,
+					},
+				},
+			},
+			initShares: []*file.Share{
+				{
+					Name: testShareName,
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName,
+					CapacityBytes:  int64(baseCap),
+				},
+				{
+					Name: testShareName + "1",
+					Parent: &file.MultishareInstance{
+						Project:  testProject,
+						Location: testRegion,
+						Name:     testInstanceName1,
+						Labels: map[string]string{
+							util.ParamMultishareInstanceScLabelKey: testInstanceScPrefix,
+						},
+						CapacityBytes: 1 * util.Tb,
+						Tier:          "Enterprise",
+						Network: file.Network{
+							Ip: testIP,
+						},
+					},
+					MountPointName: testShareName + "1",
+					CapacityBytes:  int64(mediumCap),
+				},
+			},
+			ops: []OpItem{
+				{
+					id:     "op1",
+					target: fmt.Sprintf(shareUriFmt, testProject, testRegion, testInstanceName1, testShareName+"1"),
+					verb:   "update",
+				},
+			},
+			errorExpected: true,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			var v1beta1ops []*filev1beta1multishare.Operation
+			for _, item := range tc.ops {
+				var meta filev1beta1multishare.OperationMetadata
+				meta.Target = item.target
+				meta.Verb = item.verb
+				bytes, _ := json.Marshal(meta)
+				v1beta1ops = append(v1beta1ops, &filev1beta1multishare.Operation{
+					Name:     item.id,
+					Done:     item.done,
+					Metadata: bytes,
+				})
+			}
+
+			s, err := file.NewFakeServiceForMultishare(tc.initInstance, tc.initShares, v1beta1ops)
+			if err != nil {
+				t.Fatalf("failed to fake service: %v", err)
+			}
+			cloudProvider, _ := cloud.NewFakeCloud()
+			cloudProvider.File = s
+			mcs := NewMultishareController(initTestDriver(t), s, cloudProvider, util.NewVolumeLocks(), "")
+			resp, err := mcs.ControllerExpandVolume(context.Background(), tc.req)
+			if tc.errorExpected && err == nil {
+				t.Errorf("expected error not found")
+			}
+			if !tc.errorExpected && err != nil {
+				t.Errorf("unexpected error")
+			}
+			if tc.resp != nil && resp == nil {
+				t.Errorf("mismatch in response")
+			}
+			if tc.resp == nil && resp != nil {
+				t.Errorf("mismatch in response")
+			}
+			if !reflect.DeepEqual(resp, tc.resp) {
+				t.Errorf("got resp %+v, expected %+v", resp, tc.resp)
+			}
+		})
+	}
+}
diff --git a/pkg/csi_driver/multishare_ops_manager_test.go b/pkg/csi_driver/multishare_ops_manager_test.go
index a3c5c984..64811d9a 100644
--- a/pkg/csi_driver/multishare_ops_manager_test.go
+++ b/pkg/csi_driver/multishare_ops_manager_test.go
@@ -410,10 +410,10 @@ func TestInstanceNeedsExpand(t *testing.T) {
 			cloudProvider := initCloudProviderWithBlockingFileService(t, opUnblocker)
 			manager := NewMultishareOpsManager(cloudProvider)
 
-			runRequest := func(ctx context.Context, share *file.Share) <-chan Response {
+			runRequest := func(ctx context.Context, share *file.Share, capNeeded int64) <-chan Response {
 				responseChannel := make(chan Response)
 				go func() {
-					needsExpand, targetBytes, err := manager.instanceNeedsExpand(context.Background(), share)
+					needsExpand, targetBytes, err := manager.instanceNeedsExpand(context.Background(), share, capNeeded)
 					responseChannel <- Response{
 						instanceNeedsExpand: needsExpand,
 						targetBytes:         targetBytes,
@@ -430,7 +430,7 @@ func TestInstanceNeedsExpand(t *testing.T) {
 				manager.cloud.File.StartCreateShareOp(context.Background(), &share)
 			}
 
-			respChannel := runRequest(context.Background(), tc.targetShareToAccomodate)
+			respChannel := runRequest(context.Background(), tc.targetShareToAccomodate, tc.targetShareToAccomodate.CapacityBytes)
 			response := <-respChannel
 			if tc.expectError && response.err == nil {
 				t.Errorf("expected error")
diff --git a/pkg/csi_driver/node_test.go b/pkg/csi_driver/node_test.go
index e390905e..581842aa 100644
--- a/pkg/csi_driver/node_test.go
+++ b/pkg/csi_driver/node_test.go
@@ -557,6 +557,79 @@ func TestNodeGetInfo(t *testing.T) {
 func TestNodeGetCapabilities(t *testing.T) {
 }
 
+func TestNodeGetVolumeStats(t *testing.T) {
+	testEnv := initTestNodeServer(t)
+	ns := testEnv.ns
+
+	// Setup mount target path
+	tempDir, err := ioutil.TempDir("", "ngvs")
+	if err != nil {
+		t.Fatalf("failed to setup testdir: %v", err)
+	}
+	defer os.RemoveAll(tempDir)
+	targetPath := filepath.Join(tempDir, "mount")
+	stagingPath := filepath.Join(tempDir, "staging")
+
+	req := &csi.NodePublishVolumeRequest{
+		VolumeId:          testVolumeID,
+		TargetPath:        targetPath,
+		StagingTargetPath: stagingPath,
+		Readonly:          false,
+		VolumeCapability:  testVolumeCapability,
+		VolumeContext:     testVolumeAttributes,
+	}
+	_, err = ns.NodePublishVolume(context.Background(), req)
+	if err != nil {
+		t.Fatalf("Failed to set up test by publishing default vol: %v", err)
+	}
+
+	testCases := []struct {
+		name       string
+		volumeID   string
+		volumePath string
+		expectErr  bool
+	}{
+		{
+			name:       "normal",
+			volumeID:   testVolumeID,
+			volumePath: targetPath,
+		},
+		{
+			name:       "no vol id",
+			volumePath: targetPath,
+			expectErr:  true,
+		},
+		{
+			name:      "no vol path",
+			volumeID:  testVolumeID,
+			expectErr: true,
+		},
+		{
+			name:       "bad vol path",
+			volumeID:   testVolumeID,
+			volumePath: "/mnt/fake",
+			expectErr:  true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			req := &csi.NodeGetVolumeStatsRequest{
+				VolumeId:   tc.volumeID,
+				VolumePath: tc.volumePath,
+			}
+			_, err := ns.NodeGetVolumeStats(context.Background(), req)
+			if err != nil && !tc.expectErr {
+				t.Fatalf("Got unexpected err: %v", err)
+			}
+			if err == nil && tc.expectErr {
+				t.Fatal("Did not get error but expected one")
+			}
+		})
+	}
+
+}
+
 func validateMountPoint(t *testing.T, name string, fm *mount.FakeMounter, e *mount.MountPoint) {
 	if e == nil {
 		if len(fm.MountPoints) != 0 {
diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go
index e15f9b53..918f462f 100644
--- a/pkg/util/util_test.go
+++ b/pkg/util/util_test.go
@@ -407,3 +407,63 @@ func TestParseShareURI(t *testing.T) {
 	}
 
 }
+
+func TestAlignBytes(t *testing.T) {
+	tests := []struct {
+		name        string
+		inputBytes  int64
+		stepBytes   int64
+		targetBytes int64
+	}{
+		{
+			name:        "0 step",
+			inputBytes:  100 * Gb,
+			targetBytes: 100 * Gb,
+		},
+		{
+			name:        "input less than step size",
+			stepBytes:   256 * Gb,
+			inputBytes:  100,
+			targetBytes: 256 * Gb,
+		},
+		{
+			name:        "aligned input test case 1",
+			stepBytes:   256 * Gb,
+			inputBytes:  2 * 256 * Gb,
+			targetBytes: 2 * 256 * Gb,
+		},
+		{
+			name:        "aligned input test case 2",
+			stepBytes:   256 * Gb,
+			inputBytes:  1 * Tb,
+			targetBytes: 1 * Tb,
+		},
+		{
+			name:        "aligned input test case 3",
+			stepBytes:   256 * Gb,
+			inputBytes:  10 * Tb,
+			targetBytes: 10 * Tb,
+		},
+		{
+			name:        "misaligned input test case 1",
+			stepBytes:   256 * Gb,
+			inputBytes:  256*Gb + 1,
+			targetBytes: 256 * 2 * Gb,
+		},
+		{
+			name:        "misaligned input test case 2",
+			stepBytes:   256 * Gb,
+			inputBytes:  1*Tb + 1,
+			targetBytes: 1*Tb + 256*Gb,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			got := AlignBytes(tc.inputBytes, tc.stepBytes)
+			if got != tc.targetBytes {
+				t.Errorf("got %d bytes, expected %d bytes", got, tc.targetBytes)
+			}
+		})
+	}
+
+}
diff --git a/test/e2e/tests/resize_e2e_test.go b/test/e2e/tests/resize_e2e_test.go
index 6e4f2498..bd0eb524 100644
--- a/test/e2e/tests/resize_e2e_test.go
+++ b/test/e2e/tests/resize_e2e_test.go
@@ -16,8 +16,10 @@ package tests
 
 import (
 	"fmt"
+	"math/rand"
 	"path/filepath"
 	"strings"
+	"time"
 
 	filev1beta1 "google.golang.org/api/file/v1beta1"
 	"k8s.io/apimachinery/pkg/util/uuid"
@@ -39,6 +41,7 @@ const (
 	defaultTier          = "STANDARD"
 	defaultNetwork       = "default"
 	minVolumeSize  int64 = 1 * util.Tb
+	defaultEpsilon int64 = 20 * util.Gb // when a 1T instance was created, the actual size is 1007G, so there will be ~20 Gb of space unavailable
 )
 
 var _ = Describe("Google Cloud Filestore CSI Driver", func() {
@@ -51,7 +54,7 @@ var _ = Describe("Google Cloud Filestore CSI Driver", func() {
 
 		validateDisk(diskInfo)
 
-		writeAndReadDisk(diskInfo)
+		writeAndReadDisk(diskInfo, testContext)
 
 		offlineResizeDisk(diskInfo)
 
@@ -59,7 +62,7 @@ var _ = Describe("Google Cloud Filestore CSI Driver", func() {
 	})
 })
 
-func writeAndReadDisk(di *DiskInfo) {
+func writeAndReadDisk(di *DiskInfo, tc *remote.TestContext) {
 	var err error
 	instance := di.TestCtx.Instance
 	volName := di.Name
@@ -97,6 +100,8 @@ func writeAndReadDisk(di *DiskInfo) {
 	}()
 
 	validateRead(secondPublishDir, testFileName, "test", instance)
+
+	validateVolumeStats(publishDir, di.Volume.VolumeId, instance, tc)
 }
 
 func onlineResizeDisk(di *DiskInfo) {
@@ -288,6 +293,23 @@ func validateRead(publishDir, testFileName, testFileContents string, instance *r
 	Expect(strings.TrimSpace(string(readContents))).To(Equal(testFileContents))
 }
 
+func validateVolumeStats(publishDir, volID string, instance *remote.InstanceInfo, tc *remote.TestContext) {
+	// generate a random file between 1 Mb to 100 Mb for usage verification
+	rand.Seed(time.Now().UnixNano())
+	fileSize := 1 + rand.Int63n(100)
+	err := testutils.GenerateRandomFile(instance, publishDir, fileSize)
+	Expect(err).To(BeNil(), "Failed to generate file")
+
+	available, capacity, used, inodesFree, inodes, inodesUsed, err := tc.Client.NodeGetVolumeStats(volID, publishDir)
+	Expect(err).To(BeNil(), "Failed to get node volume stats: %v", err)
+	Expect(equalWithinEpsilon(available, minVolumeSize, defaultEpsilon)).To(BeTrue())
+	Expect(equalWithinEpsilon(capacity, minVolumeSize, defaultEpsilon)).To(BeTrue())
+	Expect(used).To(Equal(util.MbToBytes(fileSize)))
+	Expect(inodesFree == 0).To(BeFalse())
+	Expect(inodes == 0).To(BeFalse())
+	Expect(inodesUsed == 0).To(BeFalse())
+}
+
 func validateDisk(di *DiskInfo) {
 	inst, err := getDisk(di)
 	Expect(err).To(BeNil(), "Could not get disk from cloud directly")
@@ -306,3 +328,10 @@ func validateDisk(di *DiskInfo) {
 		}
 	}
 }
+
+func equalWithinEpsilon(a, b, epsiolon int64) bool {
+	if a > b {
+		return a-b < epsiolon
+	}
+	return b-a < epsiolon
+}
diff --git a/test/e2e/utils/utils.go b/test/e2e/utils/utils.go
index cf4ce7e7..43f84a85 100644
--- a/test/e2e/utils/utils.go
+++ b/test/e2e/utils/utils.go
@@ -141,6 +141,15 @@ func WriteFile(instance *remote.InstanceInfo, filePath, fileContents string) err
 	return nil
 }
 
+// generate a random file with given size in MiB
+func GenerateRandomFile(instance *remote.InstanceInfo, filePath string, size int64) error {
+	output, err := instance.SSHNoSudo("dd", "if=/dev/urandom", fmt.Sprintf("of=%s/file", filePath), "bs=1MiB", fmt.Sprintf("count=%d", size))
+	if err != nil {
+		return fmt.Errorf("failed to write test file %s. Output: %v, errror: %v", filePath, output, err)
+	}
+	return nil
+}
+
 func ReadFile(instance *remote.InstanceInfo, filePath string) (string, error) {
 	output, err := instance.SSHNoSudo("cat", filePath)
 	if err != nil {
diff --git a/test/k8s-integration/driver-config.go b/test/k8s-integration/driver-config.go
index 7465c9b6..650c69f7 100644
--- a/test/k8s-integration/driver-config.go
+++ b/test/k8s-integration/driver-config.go
@@ -32,11 +32,13 @@ const (
 	// configurable timeouts for the k8s e2e testsuites.
 	podStartTimeout                 = "600s"
 	claimProvisionTimeout           = "600s"
+	pvDeleteTimeout                 = "600s"
 	enterpriseClaimProvisionTimeout = "1800s"
 
 	// These are keys for the configurable timeout map.
 	podStartTimeoutKey       = "PodStart"
 	claimProvisionTimeoutKey = "ClaimProvision"
+	pvDeleteTimeoutKey       = "PVDelete"
 )
 
 // generateDriverConfigFile loads a testdriver config template and creates a file
@@ -113,6 +115,7 @@ func generateDriverConfigFile(testParams *testParameters, storageClassFile strin
 	timeouts := map[string]string{
 		claimProvisionTimeoutKey: claimProvisionTimeout,
 		podStartTimeoutKey:       podStartTimeout,
+		pvDeleteTimeoutKey:       pvDeleteTimeout,
 	}
 	if strings.Contains(storageClassFile, enterpriseTier) {
 		timeouts[claimProvisionTimeoutKey] = enterpriseClaimProvisionTimeout
diff --git a/test/remote/client-wrappers.go b/test/remote/client-wrappers.go
index 6f392cd0..090000c1 100644
--- a/test/remote/client-wrappers.go
+++ b/test/remote/client-wrappers.go
@@ -206,6 +206,36 @@ func (c *CsiClient) ControllerExpandVolumeWithLimit(volumeID string, sizeBytes,
 	return err
 }
 
+func (c *CsiClient) NodeGetVolumeStats(volumeID, volumePath string) (available, capacity, used, inodesFree, inodes, inodesUsed int64, err error) {
+	resp, err := c.nodeClient.NodeGetVolumeStats(context.Background(), &csipb.NodeGetVolumeStatsRequest{
+		VolumeId:   volumeID,
+		VolumePath: volumePath,
+	})
+	if err != nil {
+		return
+	}
+	for _, usage := range resp.Usage {
+		if usage == nil {
+			continue
+		}
+		unit := usage.GetUnit()
+		switch unit {
+		case csipb.VolumeUsage_BYTES:
+			available = usage.GetAvailable()
+			capacity = usage.GetTotal()
+			used = usage.GetUsed()
+		case csipb.VolumeUsage_INODES:
+			inodesFree = usage.GetAvailable()
+			inodes = usage.GetTotal()
+			inodesUsed = usage.GetUsed()
+		default:
+			err = fmt.Errorf("unknown key %s in usage", unit.String())
+			return
+		}
+	}
+	return
+}
+
 func (c *CsiClient) CreateSnapshot(snapshotName, sourceVolumeId string) (string, error) {
 	csr := &csipb.CreateSnapshotRequest{
 		Name:           snapshotName,
diff --git a/test/remote/setup-teardown.go b/test/remote/setup-teardown.go
index 8b63eecd..4a5cfd01 100644
--- a/test/remote/setup-teardown.go
+++ b/test/remote/setup-teardown.go
@@ -72,7 +72,7 @@ func SetupInstance(instanceProject, instanceZone, instanceName, instanceServiceA
 }
 
 // SetupNewDriverAndClient gets the driver binary, runs it on the provided instance and connects
-// a CSI client to it through SHH tunnelling. It returns a TestContext with both a handle to the instance
+// a CSI client to it through SSH tunnelling. It returns a TestContext with both a handle to the instance
 // that the driver is on and the CSI Client object to make CSI calls to the remote driver.
 func SetupNewDriverAndClient(instance *InstanceInfo, config *ClientConfig) (*TestContext, error) {
 	archivePath, err := CreateDriverArchive(archiveName, config.PkgPath, config.BinPath)