Repository: kubernetes-csi/csi-driver-nfs
Pull request #292 (2022-03-05 13:24:58)

feat: add mountPermissions parameter in storage class
<!--  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?**
/kind feature

**What this PR does / why we need it**:
feat: add mountPermissions parameter in storage class
user could set customized mountPermissions in storage class, e.g.
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: nfs-server.default.svc.cluster.local
  share: /
  mountPermissions: "0755"
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
  - hard
  - nfsvers=4.1
```

**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
feat: add mountPermissions parameter in storage class
```



Patch:
diff --git a/docs/driver-parameters.md b/docs/driver-parameters.md
index d55c06ad..5523fc49 100644
--- a/docs/driver-parameters.md
+++ b/docs/driver-parameters.md
@@ -8,6 +8,7 @@ Name | Meaning | Example Value | Mandatory | Default value
 --- | --- | --- | --- | ---
 server | NFS Server address | domain name `nfs-server.default.svc.cluster.local` <br>or IP address `127.0.0.1` | Yes |
 share | NFS share path | `/` | Yes |
+mountPermissions | mounted folder permissions. The default is `0777` |  | No |
 
 ### PV/PVC usage (static provisioning)
 > [`PersistentVolume` example](../deploy/example/pv-nfs-csi.yaml)
@@ -16,7 +17,7 @@ Name | Meaning | Example Value | Mandatory | Default value
 --- | --- | --- | --- | ---
 volumeAttributes.server | NFS Server address | domain name `nfs-server.default.svc.cluster.local` <br>or IP address `127.0.0.1` | Yes |
 volumeAttributes.share | NFS share path | `/` |  Yes  |
-
+volumeAttributes.mountPermissions | mounted folder permissions. The default is `0777` |  | No |
 
 ### Tips
 #### provide `mountOptions` for `DeleteVolume`
diff --git a/pkg/nfs/controllerserver.go b/pkg/nfs/controllerserver.go
index 446033b3..63986cfb 100644
--- a/pkg/nfs/controllerserver.go
+++ b/pkg/nfs/controllerserver.go
@@ -21,6 +21,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/container-storage-interface/spec/lib/go/csi"
@@ -78,8 +79,32 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
 
+	mountPermissions := cs.Driver.mountPermissions
 	reqCapacity := req.GetCapacityRange().GetRequiredBytes()
-	nfsVol, err := cs.newNFSVolume(name, reqCapacity, req.GetParameters())
+	parameters := req.GetParameters()
+	if parameters == nil {
+		parameters = make(map[string]string)
+	}
+	// validate parameters (case-insensitive)
+	for k, v := range parameters {
+		switch strings.ToLower(k) {
+		case paramServer:
+			// no op
+		case paramShare:
+			// no op
+		case mountPermissionsField:
+			if v != "" {
+				var err error
+				if mountPermissions, err = strconv.ParseUint(v, 8, 32); err != nil {
+					return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid mountPermissions %s in storage class", v))
+				}
+			}
+		default:
+			return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid parameter %q in storage class", k))
+		}
+	}
+
+	nfsVol, err := cs.newNFSVolume(name, reqCapacity, parameters)
 	if err != nil {
 		return nil, status.Error(codes.InvalidArgument, err.Error())
 	}
@@ -89,7 +114,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
 		volCap = req.GetVolumeCapabilities()[0]
 	}
 	// Mount nfs base share so we can create a subdirectory
-	if err = cs.internalMount(ctx, nfsVol, volCap); err != nil {
+	if err = cs.internalMount(ctx, nfsVol, parameters, volCap); err != nil {
 		return nil, status.Errorf(codes.Internal, "failed to mount nfs server: %v", err.Error())
 	}
 	defer func() {
@@ -98,7 +123,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
 		}
 	}()
 
-	fileMode := os.FileMode(cs.Driver.mountPermissions)
+	fileMode := os.FileMode(mountPermissions)
 	// Create subdirectory under base-dir
 	internalVolumePath := cs.getInternalVolumePath(nfsVol)
 	if err = os.Mkdir(internalVolumePath, fileMode); err != nil && !os.IsExist(err) {
@@ -108,7 +133,16 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
 	if err = os.Chmod(internalVolumePath, fileMode); err != nil {
 		klog.Warningf("failed to chmod subdirectory: %v", err.Error())
 	}
-	return &csi.CreateVolumeResponse{Volume: cs.nfsVolToCSI(nfsVol)}, nil
+
+	parameters[paramServer] = nfsVol.server
+	parameters[paramShare] = cs.getVolumeSharePath(nfsVol)
+	return &csi.CreateVolumeResponse{
+		Volume: &csi.Volume{
+			VolumeId:      nfsVol.id,
+			CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
+			VolumeContext: parameters,
+		},
+	}, nil
 }
 
 // DeleteVolume delete a volume
@@ -138,7 +172,7 @@ func (cs *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVol
 	}
 
 	// mount nfs base share so we can delete the subdirectory
-	if err = cs.internalMount(ctx, nfsVol, volCap); err != nil {
+	if err = cs.internalMount(ctx, nfsVol, nil, volCap); err != nil {
 		return nil, status.Errorf(codes.Internal, "failed to mount nfs server: %v", err.Error())
 	}
 	defer func() {
@@ -254,7 +288,7 @@ func (cs *ControllerServer) validateVolumeCapability(c *csi.VolumeCapability) er
 }
 
 // Mount nfs server at base-dir
-func (cs *ControllerServer) internalMount(ctx context.Context, vol *nfsVolume, volCap *csi.VolumeCapability) error {
+func (cs *ControllerServer) internalMount(ctx context.Context, vol *nfsVolume, volumeContext map[string]string, volCap *csi.VolumeCapability) error {
 	sharePath := filepath.Join(string(filepath.Separator) + vol.baseDir)
 	targetPath := cs.getInternalMountPath(vol)
 
@@ -266,13 +300,16 @@ func (cs *ControllerServer) internalMount(ctx context.Context, vol *nfsVolume, v
 		}
 	}
 
+	if volumeContext == nil {
+		volumeContext = make(map[string]string)
+	}
+	volumeContext[paramServer] = vol.server
+	volumeContext[paramShare] = sharePath
+
 	klog.V(2).Infof("internally mounting %v:%v at %v", vol.server, sharePath, targetPath)
 	_, err := cs.Driver.ns.NodePublishVolume(ctx, &csi.NodePublishVolumeRequest{
-		TargetPath: targetPath,
-		VolumeContext: map[string]string{
-			paramServer: vol.server,
-			paramShare:  sharePath,
-		},
+		TargetPath:       targetPath,
+		VolumeContext:    volumeContext,
 		VolumeCapability: volCap,
 		VolumeId:         vol.id,
 	})
@@ -303,8 +340,6 @@ func (cs *ControllerServer) newNFSVolume(name string, size int64, params map[str
 			server = v
 		case paramShare:
 			baseDir = v
-		default:
-			return nil, fmt.Errorf("invalid parameter %q", k)
 		}
 	}
 
@@ -347,18 +382,6 @@ func (cs *ControllerServer) getVolumeSharePath(vol *nfsVolume) string {
 	return filepath.Join(string(filepath.Separator), vol.baseDir, vol.subDir)
 }
 
-// Convert into nfsVolume into a csi.Volume
-func (cs *ControllerServer) nfsVolToCSI(vol *nfsVolume) *csi.Volume {
-	return &csi.Volume{
-		CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
-		VolumeId:      vol.id,
-		VolumeContext: map[string]string{
-			paramServer: vol.server,
-			paramShare:  cs.getVolumeSharePath(vol),
-		},
-	}
-}
-
 // Given a nfsVolume, return a CSI volume id
 func (cs *ControllerServer) getVolumeIDFromNfsVol(vol *nfsVolume) string {
 	idElements := make([]string, totalIDElements)
diff --git a/pkg/nfs/nfs.go b/pkg/nfs/nfs.go
index 8c3d4e74..0de496a1 100644
--- a/pkg/nfs/nfs.go
+++ b/pkg/nfs/nfs.go
@@ -55,8 +55,9 @@ const (
 	// The base directory must be a direct child of the root directory.
 	// The root directory is omitted from the string, for example:
 	//     "base" instead of "/base"
-	paramShare        = "share"
-	mountOptionsField = "mountoptions"
+	paramShare            = "share"
+	mountOptionsField     = "mountoptions"
+	mountPermissionsField = "mountpermissions"
 )
 
 func NewDriver(options *DriverOptions) *Driver {
diff --git a/pkg/nfs/nodeserver.go b/pkg/nfs/nodeserver.go
index af5c8331..86f1dab8 100644
--- a/pkg/nfs/nodeserver.go
+++ b/pkg/nfs/nodeserver.go
@@ -19,6 +19,7 @@ package nfs
 import (
 	"fmt"
 	"os"
+	"strconv"
 	"strings"
 
 	"github.com/container-storage-interface/spec/lib/go/csi"
@@ -56,6 +57,7 @@ func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
 	}
 
 	var server, baseDir string
+	mountPermissions := ns.Driver.mountPermissions
 	for k, v := range req.GetVolumeContext() {
 		switch strings.ToLower(k) {
 		case paramServer:
@@ -66,6 +68,13 @@ func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
 			if v != "" {
 				mountOptions = append(mountOptions, v)
 			}
+		case mountPermissionsField:
+			if v != "" {
+				var err error
+				if mountPermissions, err = strconv.ParseUint(v, 8, 32); err != nil {
+					return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid mountPermissions %s", v))
+				}
+			}
 		}
 	}
 
@@ -80,7 +89,7 @@ func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
 	notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath)
 	if err != nil {
 		if os.IsNotExist(err) {
-			if err := os.MkdirAll(targetPath, os.FileMode(ns.Driver.mountPermissions)); err != nil {
+			if err := os.MkdirAll(targetPath, os.FileMode(mountPermissions)); err != nil {
 				return nil, status.Error(codes.Internal, err.Error())
 			}
 			notMnt = true
@@ -104,8 +113,8 @@ func (ns *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis
 		return nil, status.Error(codes.Internal, err.Error())
 	}
 
-	klog.V(2).Infof("volumeID(%v): mount targetPath(%s) with permissions(0%o)", volumeID, targetPath, ns.Driver.mountPermissions)
-	if err := os.Chmod(targetPath, os.FileMode(ns.Driver.mountPermissions)); err != nil {
+	klog.V(2).Infof("volumeID(%v): mount targetPath(%s) with permissions(0%o)", volumeID, targetPath, mountPermissions)
+	if err := os.Chmod(targetPath, os.FileMode(mountPermissions)); err != nil {
 		return nil, status.Error(codes.Internal, err.Error())
 	}
 	return &csi.NodePublishVolumeResponse{}, nil


Test patch:
diff --git a/pkg/nfs/controllerserver_test.go b/pkg/nfs/controllerserver_test.go
index d7fb6309..8b104101 100644
--- a/pkg/nfs/controllerserver_test.go
+++ b/pkg/nfs/controllerserver_test.go
@@ -100,16 +100,18 @@ func TestCreateVolume(t *testing.T) {
 					},
 				},
 				Parameters: map[string]string{
-					paramServer: testServer,
-					paramShare:  testBaseDir,
+					paramServer:           testServer,
+					paramShare:            testBaseDir,
+					mountPermissionsField: "0750",
 				},
 			},
 			resp: &csi.CreateVolumeResponse{
 				Volume: &csi.Volume{
 					VolumeId: newTestVolumeID,
 					VolumeContext: map[string]string{
-						paramServer: testServer,
-						paramShare:  testShare,
+						paramServer:           testServer,
+						paramShare:            testShare,
+						mountPermissionsField: "0750",
 					},
 				},
 			},
@@ -201,6 +203,28 @@ func TestCreateVolume(t *testing.T) {
 			},
 			expectErr: true,
 		},
+		{
+			name: "[Error] invalid mountPermissions",
+			req: &csi.CreateVolumeRequest{
+				Name: testCSIVolume,
+				VolumeCapabilities: []*csi.VolumeCapability{
+					{
+						AccessType: &csi.VolumeCapability_Mount{
+							Mount: &csi.VolumeCapability_MountVolume{},
+						},
+						AccessMode: &csi.VolumeCapability_AccessMode{
+							Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,
+						},
+					},
+				},
+				Parameters: map[string]string{
+					paramServer:           testServer,
+					paramShare:            testBaseDir,
+					mountPermissionsField: "07ab",
+				},
+			},
+			expectErr: true,
+		},
 	}
 
 	for _, test := range cases {
diff --git a/pkg/nfs/nodeserver_test.go b/pkg/nfs/nodeserver_test.go
index d417f098..a559c61e 100644
--- a/pkg/nfs/nodeserver_test.go
+++ b/pkg/nfs/nodeserver_test.go
@@ -41,8 +41,15 @@ func TestNodePublishVolume(t *testing.T) {
 	}
 
 	params := map[string]string{
-		"server": "server",
-		"share":  "share",
+		"server":              "server",
+		"share":               "share",
+		mountPermissionsField: "0755",
+	}
+
+	invalidParams := map[string]string{
+		"server":              "server",
+		"share":               "share",
+		mountPermissionsField: "07ab",
 	}
 
 	volumeCap := csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER}
@@ -112,6 +119,16 @@ func TestNodePublishVolume(t *testing.T) {
 				Readonly:         true},
 			expectedErr: nil,
 		},
+		{
+			desc: "[Error] invalid mountPermissions",
+			req: csi.NodePublishVolumeRequest{
+				VolumeContext:    invalidParams,
+				VolumeCapability: &csi.VolumeCapability{AccessMode: &volumeCap},
+				VolumeId:         "vol_1",
+				TargetPath:       targetTest,
+				Readonly:         true},
+			expectedErr: status.Error(codes.InvalidArgument, "invalid mountPermissions 07ab"),
+		},
 	}
 
 	// setup
diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go
index 2660eb84..4dfd0b4d 100644
--- a/test/e2e/e2e_suite_test.go
+++ b/test/e2e/e2e_suite_test.go
@@ -50,6 +50,7 @@ var (
 		"share":  nfsShare,
 		"csi.storage.k8s.io/provisioner-secret-name":      "mount-options",
 		"csi.storage.k8s.io/provisioner-secret-namespace": "default",
+		"mountPermissions": "0755",
 	}
 	controllerServer *nfs.ControllerServer
 )