Repository: dell/csi-baremetal (go)

Issue:
Add mountOptions support
**Is your feature request related to a problem? Please describe.**
Need to use mountOptions for NodePublishRequest

SC example
```
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: {{ .Values.storageClass.name }}
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
# CSI driver name
provisioner: csi-baremetal  
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
parameters:
  # With ANY storage type CSI allocates volumes on top of ANY physical drive (non LVG)
  storageType: ANY
  fsType: xfs
mountOptions:
  - noatime
```
Current behavior
```
mount /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-0841ef31-ade5-4e02-ba9b-306d301da695/globalmount/dev /var/lib/kubelet/pods/d4f92fad-51fc-4d8a-b1ac-c97b99f11bcf/volumes/kubernetes.io~csi/pvc-0841ef31-ade5-4e02-ba9b-306d301da695/mount
```
Required
```
mount -o noatime /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-0841ef31-ade5-4e02-ba9b-306d301da695/globalmount/dev /var/lib/kubelet/pods/d4f92fad-51fc-4d8a-b1ac-c97b99f11bcf/volumes/kubernetes.io~csi/pvc-0841ef31-ade5-4e02-ba9b-306d301da695/mount
```

**Describe the solution you'd like**
1. Passed mountOptions from PV to CMD Executor on NodePublish
2. Check supported PV options on CreateVolume


Pull request description:
[ISSUE-625] Add mount options support
## Purpose
### Resolves #625

- Check supported mount options on CreateVolume Request
- Add "noatime" option for Publish

## PR checklist
- [x] Add link to the issue
- [x] Choose Project
- [x] Choose PR label
- [x] New unit tests added
- [x] Modified code has meaningful comments
- [ ] All TODOs are linked with the issues
- [ ] All comments are resolved

## Testing
```
user@user-vm ~/g/s/g/d/csi-baremetal (feature-issue-625-add-mount-options-support)> kd sc
Name:                  csi-baremetal-sc
IsDefaultClass:        Yes
Annotations:           meta.helm.sh/release-name=csi-baremetal,meta.helm.sh/release-namespace=default,storageclass.kubernetes.io/is-default-class=true
Provisioner:           csi-baremetal
Parameters:            fsType=xfs,storageType=ANY
AllowVolumeExpansion:  <unset>
MountOptions:
  noatime
ReclaimPolicy:      Delete
VolumeBindingMode:  WaitForFirstConsumer
Events:             <none>

user@user-vm ~/g/s/g/dell> kgo volumes
NAME                                       SIZE        STORAGE CLASS   HEALTH   CSI_STATUS   OP_STATUS   USAGE    TYPE   LOCATION                               NODE
pvc-7c3e0bd4-eef2-4b3f-9afb-526224522c68   105906176   HDD             GOOD     PUBLISHED    OPERATIVE   IN_USE   xfs    aa804c75-49d2-48f8-adb6-944844c5b8ca   6bcf9136-4a7c-4a6e-ac09-32e09485db19
pvc-b6fdde0b-65b8-46b6-b076-d43f4a72747e   105906176   HDD             GOOD     PUBLISHED    OPERATIVE   IN_USE   xfs    db46ff55-06e0-43a6-b953-bf722f993e7c   6bcf9136-4a7c-4a6e-ac09-32e09485db19
pvc-eed07b95-9140-4c45-a16e-45466b102478   105906176   HDD             GOOD     PUBLISHED    OPERATIVE   IN_USE   xfs    fd6e5122-7da7-470b-adef-62c7b436d8a1   6bcf9136-4a7c-4a6e-ac09-32e09485db19

Dec  2 08:46:17.038 [DEBU] [Executor] [mount -o noatime /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-eed07b95-9140-4c45-a16e-45466b102478/globalmount/dev /var/lib/kubelet/pods/b29cf029-f14d-4336-9bd0-b3837e268f33/volumes/kubernetes.io~csi/pvc-eed07b95-9140-4c45-a16e-45466b102478/mount] [93.518338ms] [93518338] stdout: 
```


Patch:
diff --git a/Makefile.validation b/Makefile.validation
index a73fb9e..dee042e 100644
--- a/Makefile.validation
+++ b/Makefile.validation
@@ -47,9 +47,17 @@ test-sanity:
 #	should fail when requesting to create a volume with already existing name and different capacity|\
 #	should not fail when requesting to create a volume with already existing name and same capacity" -ginkgo.v -timeout=0
 
-load-operator-image:
+load-operator-image: pull-operator-image \
+kind-tag-operator-image \
+kind-load-operator-image 
+
+pull-operator-image:
 	docker pull ${REGISTRY}/${PROJECT}-${OPERATOR}:${OPERATOR_VERSION}
+
+kind-tag-operator-image:	
 	docker tag ${REGISTRY}/${PROJECT}-${OPERATOR}:${OPERATOR_VERSION} ${PROJECT}-${OPERATOR}:${OPERATOR_VERSION}
+
+kind-load-operator-image:
 	$(KIND) load docker-image ${PROJECT}-${OPERATOR}:${OPERATOR_VERSION}
 
 kind-pull-images: kind-pull-sidecar-images
@@ -138,7 +146,7 @@ kind-install: kind-build
 	kind version
 
 kind-create-cluster:
-	$(KIND) create cluster --config $(KIND_DIR)/kind.yaml --image kindest/node:v1.18.2
+	$(KIND) create cluster --config $(KIND_DIR)/$(KIND_CONFIG) --image kindest/node:$(KIND_IMAGE_VERSION) --wait $(KIND_WAIT)
 
 kind-delete-cluster:
 	$(KIND) delete cluster
diff --git a/docs/README.md b/docs/README.md
index a63024c..7e48247 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,4 +1,5 @@
 [![PR validation](https://github.com/dell/csi-baremetal/actions/workflows/pr.yml/badge.svg)](https://github.com/dell/csi-baremetal/actions/workflows/pr.yml)
+[![E2E testing](https://github.com/dell/csi-baremetal/actions/workflows/e2e.yml/badge.svg)](https://github.com/dell/csi-baremetal/actions/workflows/e2e.yml)
 [![codecov](https://codecov.io/gh/dell/csi-baremetal/branch/master/graph/badge.svg)](https://codecov.io/gh/dell/csi-baremetal)
 
 Bare-metal CSI Driver
diff --git a/docs/supported-mount-options.md b/docs/supported-mount-options.md
new file mode 100644
index 0000000..e603b6b
--- /dev/null
+++ b/docs/supported-mount-options.md
@@ -0,0 +1,29 @@
+# Supported Mount Options
+
+## Usage
+User can set options in Storage Class in mountOptions section. 
+They will be applied for all Volumes with the following SC.
+
+Example:
+```
+apiVersion: storage.k8s.io/v1
+kind: StorageClass
+metadata:
+  name: sc
+provisioner: csi-baremetal  
+reclaimPolicy: Delete
+volumeBindingMode: WaitForFirstConsumer
+parameters:
+  storageType: ANY
+  fsType: xfs
+mountOptions:
+  - noatime
+```
+
+## List of supported options
+
+- Name: noatime
+  
+    Effect: Add "noatime" option to mount command on NodePublishRequest
+    
+    Example: `mount -o noatime /src /dst`
diff --git a/pkg/base/linuxutils/fs/fs_helper.go b/pkg/base/linuxutils/fs/fs_helper.go
index c826721..1f86655 100644
--- a/pkg/base/linuxutils/fs/fs_helper.go
+++ b/pkg/base/linuxutils/fs/fs_helper.go
@@ -66,6 +66,8 @@ const (
 	UnmountCmdTmpl = "umount %s"
 	// BindOption option for mount operation
 	BindOption = "--bind"
+	// MountOptionsFlag flag to set mount options
+	MountOptionsFlag = "-o"
 )
 
 // WrapFS is an interface that encapsulates operation with file systems
diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go
index 989ae15..b0fc976 100644
--- a/pkg/controller/controller.go
+++ b/pkg/controller/controller.go
@@ -40,6 +40,7 @@ import (
 	"github.com/dell/csi-baremetal/pkg/base/k8s"
 	"github.com/dell/csi-baremetal/pkg/base/util"
 	"github.com/dell/csi-baremetal/pkg/common"
+	"github.com/dell/csi-baremetal/pkg/controller/mountoptions"
 	"github.com/dell/csi-baremetal/pkg/controller/node"
 	csibmnodeconst "github.com/dell/csi-baremetal/pkg/crcontrollers/node/common"
 )
@@ -196,6 +197,13 @@ func (c *CSIControllerService) CreateVolume(ctx context.Context, req *csi.Create
 		// ext4 by default (from request)
 		fsType = strings.ToLower(accessType.Mount.FsType)
 		mode = apiV1.ModeFS
+
+		// check mountFlags
+		if !mountoptions.IsOptionsSupported(accessType.Mount.GetMountFlags()) {
+			err = fmt.Errorf("mountOptions are not supported: %+v", accessType.Mount.GetMountFlags())
+			ll.Errorf("Failed to create volume: %v", err)
+			return nil, err
+		}
 	}
 
 	// The additional raw mode, perform only if VolumeCapability_Block (the if block above skipped) and SC has specific parameter
diff --git a/pkg/controller/mountoptions/mount_options.go b/pkg/controller/mountoptions/mount_options.go
new file mode 100644
index 0000000..a782907
--- /dev/null
+++ b/pkg/controller/mountoptions/mount_options.go
@@ -0,0 +1,74 @@
+package mountoptions
+
+/*
+	In this package placed supported mount options pass from SCs
+	Example:
+		apiVersion: storage.k8s.io/v1
+		kind: StorageClass
+		metadata:
+		  name: sc1
+		mountOptions:
+		  - noatime
+*/
+
+// MountOptionType type to clarify option purpose
+type MountOptionType string
+
+const (
+	// PublishCmdOpt are options for mount func on NodePublishRequest
+	// Example: mount -o <PublishCmdOpt> /src /dst
+	PublishCmdOpt = MountOptionType("cmdOpt")
+)
+
+// MountOption describes mount option
+type MountOption struct {
+	arg       string
+	mountType MountOptionType
+}
+
+const (
+	noatimeOpt = "noatime"
+)
+
+var (
+	// supportedMountOption contains all supported options
+	// map[optName]{optArg, optType}
+	// optName - passed from SC
+	// optArg - cmd string
+	supportedMountOption = map[string]MountOption{
+		noatimeOpt: {
+			arg:       noatimeOpt,
+			mountType: PublishCmdOpt,
+		},
+	}
+)
+
+// IsOptionSupported returns true if option in supportedMountOption
+func IsOptionSupported(option string) bool {
+	_, ok := supportedMountOption[option]
+	return ok
+}
+
+// IsOptionsSupported returns true if all options in supportedMountOption
+func IsOptionsSupported(options []string) bool {
+	for _, option := range options {
+		if !IsOptionSupported(option) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// FilterWithType returns all option from list with passed type
+func FilterWithType(mountType MountOptionType, options []string) (filteredOptions []string) {
+	for _, option := range options {
+		if val, ok := supportedMountOption[option]; ok {
+			if mountType == val.mountType {
+				filteredOptions = append(filteredOptions, val.arg)
+			}
+		}
+	}
+
+	return
+}
diff --git a/pkg/mocks/provisioners/fs_operations.go b/pkg/mocks/provisioners/fs_operations.go
index 12025f5..e9dd9e5 100644
--- a/pkg/mocks/provisioners/fs_operations.go
+++ b/pkg/mocks/provisioners/fs_operations.go
@@ -27,7 +27,7 @@ type MockFsOpts struct {
 }
 
 // PrepareAndPerformMount is a mock implementation
-func (m *MockFsOpts) PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool) error {
+func (m *MockFsOpts) PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool, mountOptions ...string) error {
 	args := m.Mock.Called(src, dst, bindMount, dstIsDir)
 
 	return args.Error(0)
diff --git a/pkg/node/node.go b/pkg/node/node.go
index c1553bf..5206812 100644
--- a/pkg/node/node.go
+++ b/pkg/node/node.go
@@ -45,6 +45,7 @@ import (
 	"github.com/dell/csi-baremetal/pkg/base/util"
 	"github.com/dell/csi-baremetal/pkg/common"
 	"github.com/dell/csi-baremetal/pkg/controller"
+	"github.com/dell/csi-baremetal/pkg/controller/mountoptions"
 	csibmnodeconst "github.com/dell/csi-baremetal/pkg/crcontrollers/node/common"
 	"github.com/dell/csi-baremetal/pkg/eventing"
 )
@@ -354,10 +355,15 @@ func (s *CSINodeService) NodePublishVolume(ctx context.Context, req *csi.NodePub
 		return nil, status.Error(codes.InvalidArgument, "Target Path missing in request")
 	}
 	var (
-		inline bool
-		err    error
+		inline       bool
+		err          error
+		mountOptions []string
 	)
 
+	if accessType, ok := req.GetVolumeCapability().AccessType.(*csi.VolumeCapability_Mount); ok {
+		mountOptions = mountoptions.FilterWithType(mountoptions.PublishCmdOpt, accessType.Mount.GetMountFlags())
+	}
+
 	if req.GetVolumeContext() != nil {
 		val, ok := req.GetVolumeContext()[EphemeralKey]
 		if ok {
@@ -418,7 +424,7 @@ func (s *CSINodeService) NodePublishVolume(ctx context.Context, req *csi.NodePub
 		}
 	} else {
 		_, isBlock := req.GetVolumeCapability().GetAccessType().(*csi.VolumeCapability_Block)
-		if err := s.fsOps.PrepareAndPerformMount(srcPath, dstPath, isBlock, !isBlock); err != nil {
+		if err := s.fsOps.PrepareAndPerformMount(srcPath, dstPath, isBlock, !isBlock, mountOptions...); err != nil {
 			ll.Errorf("Unable to mount volume: %v", err)
 			newStatus = apiV1.Failed
 			resp, errToReturn = nil, fmt.Errorf("failed to publish volume: mount error %s", err.Error())
diff --git a/pkg/node/provisioners/utilwrappers/fs_operations.go b/pkg/node/provisioners/utilwrappers/fs_operations.go
index 653919c..d0c39db 100644
--- a/pkg/node/provisioners/utilwrappers/fs_operations.go
+++ b/pkg/node/provisioners/utilwrappers/fs_operations.go
@@ -30,7 +30,7 @@ import (
 type FSOperations interface {
 	// PrepareAndPerformMount composite methods which is prepare source and destination directories
 	// and performs mount operation from src to dst
-	PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool) error
+	PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool, mountOptions ...string) error
 	// MountFakeTmpfs does attach of a temporary folder on failure
 	MountFakeTmpfs(volumeID, dst string) error
 	// UnmountWithCheck unmount operation
@@ -58,7 +58,7 @@ func NewFSOperationsImpl(e command.CmdExecutor, log *logrus.Logger) *FSOperation
 // create (if isn't exist) dst folder on node and perform mount from src to dst
 // if bindMount set to true - mount operation will contain "--bind" option
 // if error occurs and dst has created during current method call then dst will be removed
-func (fsOp *FSOperationsImpl) PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool) error {
+func (fsOp *FSOperationsImpl) PrepareAndPerformMount(src, dst string, bindMount, dstIsDir bool, mountOptions ...string) error {
 	ll := fsOp.log.WithFields(logrus.Fields{
 		"method": "PrepareAndPerformMount",
 	})
@@ -94,11 +94,13 @@ func (fsOp *FSOperationsImpl) PrepareAndPerformMount(src, dst string, bindMount,
 		}
 	}
 
-	var opts string
+	var bindOpt string
 	if bindMount {
-		opts = fs.BindOption
+		bindOpt = fs.BindOption
 	}
-	if err := fsOp.Mount(src, dst, opts); err != nil {
+
+	strMountOptions := addMountOptions(mountOptions)
+	if err := fsOp.Mount(src, dst, bindOpt, strMountOptions); err != nil {
 		if wasCreated {
 			_ = fsOp.RmDir(dst)
 		}
@@ -214,3 +216,19 @@ func (fsOp *FSOperationsImpl) CreateFSIfNotExist(fsType fs.FileSystem, device st
 
 	return nil
 }
+
+// Add options to mount command
+// Example: <mount> -o option1,option2 ...
+func addMountOptions(mountOptions []string) (opts string) {
+	if len(mountOptions) > 0 {
+		opts += fs.MountOptionsFlag + " "
+		for i, opt := range mountOptions {
+			opts += opt
+			if i != len(mountOptions)-1 {
+				opts += ","
+			}
+		}
+	}
+
+	return
+}
diff --git a/variables.mk b/variables.mk
index a37e939..d5d9b2c 100644
--- a/variables.mk
+++ b/variables.mk
@@ -75,6 +75,9 @@ LDFLAGS := -ldflags "-X ${METRICS_PACKAGE}.Revision=${RELEASE_STR} -X ${METRICS_
 KIND_DIR := test/kind
 KIND     := ${KIND_DIR}/kind
 KIND_VER := 0.8.1
+KIND_CONFIG := kind.yaml
+KIND_IMAGE_VERSION := v1.18.2
+KIND_WAIT := 30s 
 
 ### ci vars
 # timeout for short test suite, must be parsable as Go time.Duration (60m, 2h)


Test patch:
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 0000000..3ec235e
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,199 @@
+name: e2e
+
+on:
+  push:
+    branches: [ master ]
+  issue_comment:
+    types: [created]
+
+env:
+  REGISTRY: 'ghcr.io/dell/csi-baremetal'
+  go_version: '1.16.8'
+  golangci_version: '1.37.0'
+  helm_version: '3.7.1'
+  kubectl_version: 'v1.22.2'
+  kind_config: 'small-kind.yaml'
+  kind_image: 'v1.19.11'
+  kind_wait: '30s'
+jobs:
+  e2e:
+    if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/ci')
+    runs-on: ubuntu-20.04
+    steps:
+    - name: Get operator branch 
+      uses: actions/github-script@v5
+      id: set-branch
+      with:
+        result-encoding: string
+        script: |
+          var branch = `${{ github.event.comment.body }}`.split(String.fromCharCode(0x0A))[1]
+          var key = `operator_branch=`
+          if (branch){
+            if (branch.search(key) !== -1){
+              return branch.split(key)[1]
+            }else {
+              return 'keyword'
+            }
+          }
+          return ''
+
+    - name: Check out code
+      uses: actions/checkout@v2
+      
+    - name: Add csi-baremetal-operator
+      id: checkout_branch
+      uses: actions/checkout@v2
+      with:
+        repository: dell/csi-baremetal-operator 
+        ref: '${{steps.set-branch.outputs.result}}'
+        path: ./csi-baremetal-operator 
+      continue-on-error: true 
+    
+    - name: Create answer body
+      uses: actions/github-script@v5
+      id: set-answer
+      with:
+        result-encoding: string
+        script: |
+          var body = "Start CI"
+          if ('${{steps.checkout_branch.outcome}}' === 'failure'){
+            if ('${{steps.set-branch.outputs.result}}' !== 'keyword'){
+              body = 'Branch doesnt exist'
+            }else{
+              body = 'Keyword error'
+            }
+          }
+          return body
+
+    - name: Send message
+      uses: actions-ecosystem/action-create-comment@v1
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        body: |
+          @${{ github.actor }} ${{steps.set-answer.outputs.result}}.
+          Parameters: operator_branch='${{steps.set-branch.outputs.result}}'
+
+    - name: Check on failures
+      if: steps.checkout_branch.outcome != 'success'
+      run: exit 1
+
+    - name: Set environment variables
+      run: |
+        echo "CSI_BAREMETAL_DIR=$RUNNER_WORKSPACE/csi-baremetal/" >> $GITHUB_ENV
+        echo "CSI_BAREMETAL_OPERATOR_DIR=$RUNNER_WORKSPACE/csi-baremetal/csi-baremetal-operator/" >> $GITHUB_ENV
+
+    - name: LVM2 install
+      run: sudo apt-get install -y lvm2
+
+    - name: Prepare Golang 
+      uses: actions/setup-go@v2
+      with:
+        go-version: ${{env.go_version}}
+
+    - name: Install helm
+      uses: azure/setup-helm@v1
+      with:
+        version: ${{env.helm_version}}
+          
+    - name: Kubectl install
+      uses: azure/setup-kubectl@v1
+      with:
+        version: ${{env.kubectl_version}} 
+
+    - name: add CSI_VERSION
+      run: |
+        cd ${{env.CSI_BAREMETAL_DIR}}
+        echo "CSI_VERSION=`make version`" >> $GITHUB_ENV
+
+    - name: Get dependencies
+      run: make dependency
+
+    #TODO: Generate CRD - #618
+
+    - name: Build binary
+      run: |
+        make build
+        make DRIVE_MANAGER_TYPE=loopbackmgr build-drivemgr
+
+    #TODO: Install and compile proto - #613
+
+    - name: Download health-probe
+      run: make download-grpc-health-probe
+
+    - name: Build docker images
+      run: |
+        make images REGISTRY=${{ env.REGISTRY }}
+        make DRIVE_MANAGER_TYPE=loopbackmgr image-drivemgr REGISTRY=${{ env.REGISTRY }}
+    
+    - name: docker-build operators 
+      run: |
+        cd ${{env.CSI_BAREMETAL_OPERATOR_DIR}}
+        echo "CSI_OPERATOR_VERSION=`make version`" >> $GITHUB_ENV
+        make docker-build REGISTRY=${{ env.REGISTRY }}
+
+    - name: Kind preparation
+      run: |
+        cd ${{env.CSI_BAREMETAL_DIR}}
+        make kind-build KIND_VER=0.11.1
+        make kind-create-cluster KIND_CONFIG=${{env.kind_config}} KIND_IMAGE_VERSION=${{env.kind_image}} KIND_WAIT=${{env.kind_wait}}
+        kubectl cluster-info --context kind-kind
+        kubectl get pods -o wide --all-namespaces 
+        echo "current-context:" $(kubectl config current-context)
+        echo "environment-kubeconfig:" ${KUBECONFIG}
+      
+    - name: Sidecars preparing
+      run: |
+        make deps-docker-pull
+        make deps-docker-tag
+
+    - name: Retag CSI images and load them to kind
+      run: |
+        docker images 
+        make kind-tag-images TAG=$CSI_VERSION REGISTRY=${{ env.REGISTRY }}
+        make kind-load-images TAG=$CSI_VERSION REGISTRY=${{ env.REGISTRY }}
+        make kind-tag-operator-image OPERATOR_VERSION=$CSI_OPERATOR_VERSION REGISTRY=${{ env.REGISTRY }}
+        make kind-load-operator-image OPERATOR_VERSION=$CSI_OPERATOR_VERSION 
+
+    - name: Make test
+      run: make test-short-ci CSI_VERSION=$CSI_VERSION OPERATOR_VERSION=$CSI_OPERATOR_VERSION CHARTS_DIR=${{env.CSI_BAREMETAL_OPERATOR_DIR}}charts
+
+    - name: Upload report to artifacts
+      uses: actions/upload-artifact@v2.2.1
+      with:
+        name: report.xml
+        path: test/e2e/report.xml
+    
+    - name: Upload log to artifacts
+      uses: actions/upload-artifact@v2.2.1
+      with:
+        name: log.txt
+        path: log.txt
+
+  result_job:
+    needs: e2e
+    if: always() 
+    runs-on: ubuntu-20.04
+    steps:
+    - name: Create answer body 
+      uses: actions/github-script@v5
+      id: set-answer
+      with:
+        result-encoding: string
+        script: |
+          var body
+          if ('${{needs.e2e.result}}' === 'failure' ){
+            body = 'CI tests failed'
+          }else if ('${{needs.e2e.result}}' === 'success'){
+            body = 'CI tests passed'
+          }else{
+            body = 'CI tests canceled'
+          }
+          return body
+
+    - name: answer
+      uses: actions-ecosystem/action-create-comment@v1
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        body: |
+          @${{ github.actor }}, ${{steps.set-answer.outputs.result}}
+
diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go
index 43f0b99..d152fea 100644
--- a/pkg/controller/controller_test.go
+++ b/pkg/controller/controller_test.go
@@ -375,6 +375,31 @@ var _ = Describe("CSIControllerService CreateVolume", func() {
 			Expect(resp.Volume.VolumeId).To(Equal(uuid))
 			Expect(resp.Volume.CapacityBytes).To(Equal(int64(1024 * 60)))
 		})
+		It("Volume is created with supported option", func() {
+			err := testutils.AddAC(controller.k8sclient, testAC1.DeepCopy(), testAC2.DeepCopy())
+			Expect(err).To(BeNil())
+			err = controller.k8sclient.Create(testCtx, testPVC1.DeepCopy())
+			Expect(err).To(BeNil())
+			err = controller.k8sclient.CreateCR(testCtx, testACR1.Name, testACR1.DeepCopy())
+			Expect(err).To(BeNil())
+			var (
+				capacity = int64(1024 * 53)
+				req      = getCreateVolumeRequest("req1", capacity, testNode1Name, testPVC1Name, false, false, "noatime")
+				vol      = &vcrd.Volume{}
+			)
+
+			go testutils.VolumeReconcileImitation(controller.k8sclient, "req1", testNs, apiV1.Created)
+
+			resp, err := controller.CreateVolume(context.Background(), req)
+			Expect(err).To(BeNil())
+			Expect(resp).ToNot(BeNil())
+
+			err = controller.k8sclient.ReadCR(context.Background(), "req1", testNs, vol)
+			Expect(err).To(BeNil())
+			Expect(vol.Spec.CSIStatus).To(Equal(apiV1.Created))
+			Expect(vol.Spec.Mode).To(Equal(apiV1.ModeFS))
+			Expect(vol.Labels[k8s.AppLabelKey]).To(Equal(testApp))
+		})
 	})
 })
 
@@ -440,6 +465,24 @@ var _ = Describe("CSIControllerService DeleteVolume", func() {
 			Expect(err).To(BeNil())
 			Expect(volumeCrd.Spec.CSIStatus).To(Equal(apiV1.Failed))
 		})
+		It("Volume is created with unsupported option", func() {
+			err := testutils.AddAC(controller.k8sclient, testAC1.DeepCopy(), testAC2.DeepCopy())
+			Expect(err).To(BeNil())
+			err = controller.k8sclient.Create(testCtx, testPVC1.DeepCopy())
+			Expect(err).To(BeNil())
+			err = controller.k8sclient.CreateCR(testCtx, testACR1.Name, testACR1.DeepCopy())
+			Expect(err).To(BeNil())
+			var (
+				capacity = int64(1024 * 53)
+				req      = getCreateVolumeRequest("req1", capacity, testNode1Name, testPVC1Name, false, false, "someOpt1", "someOpt2")
+			)
+
+			go testutils.VolumeReconcileImitation(controller.k8sclient, "req1", testNs, apiV1.Created)
+
+			resp, err := controller.CreateVolume(context.Background(), req)
+			Expect(err).ToNot(BeNil())
+			Expect(resp).To(BeNil())
+		})
 	})
 
 	Context("Success scenarios", func() {
@@ -874,7 +917,7 @@ func fillCache(controller *CSIControllerService, volumeID, namespace string) {
 }
 
 // return CreateVolumeRequest based on provided parameters
-func getCreateVolumeRequest(name string, cap int64, preferredNode string, claimName string, needBlock, needPart bool) *csi.CreateVolumeRequest {
+func getCreateVolumeRequest(name string, cap int64, preferredNode string, claimName string, needBlock, needPart bool, mountFlags ...string) *csi.CreateVolumeRequest {
 	parameters := map[string]string{
 		util.ClaimNamespaceKey: testNs,
 		util.ClaimNameKey:      claimName,
@@ -900,7 +943,7 @@ func getCreateVolumeRequest(name string, cap int64, preferredNode string, claimN
 		accessType := &csi.VolumeCapability_Mount{
 			Mount: &csi.VolumeCapability_MountVolume{
 				FsType:     string(fs.XFS),
-				MountFlags: nil,
+				MountFlags: mountFlags,
 			},
 		}
 		req.VolumeCapabilities[0].AccessType = accessType
diff --git a/pkg/node/provisioners/utilwrappers/fs_operations_test.go b/pkg/node/provisioners/utilwrappers/fs_operations_test.go
index 7698812..a36a38b 100644
--- a/pkg/node/provisioners/utilwrappers/fs_operations_test.go
+++ b/pkg/node/provisioners/utilwrappers/fs_operations_test.go
@@ -34,7 +34,7 @@ func TestFSOperationsImpl_PrepareAndPerformMount_Success(t *testing.T) {
 		wrapFS     = &mocklu.MockWrapFS{}
 		dst        = "~/some/unusual/name"
 		src        = "/tmp"
-		bindOption = []string{""} // for bind == false
+		bindOption = []string{"", ""} // for bind == false
 		err        error
 	)
 	fsOps.WrapFS = wrapFS
@@ -53,7 +53,7 @@ func TestFSOperationsImpl_PrepareAndPerformMount_Success(t *testing.T) {
 
 	// dst folder is exist and isn't a mount point, also use bind = true
 	wrapFS.On("IsMounted", dst).Return(false, nil).Once()
-	wrapFS.On("Mount", src, dst, []string{fs.BindOption}).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, []string{fs.BindOption, ""}).Return(nil).Once()
 
 	err = fsOps.PrepareAndPerformMount(src, dst, true, true)
 	wrapFS.AssertCalled(t, "IsMounted", dst)
@@ -65,7 +65,7 @@ func TestFSOperationsImpl_PrepareAndPerformMount_Fail(t *testing.T) {
 		wrapFS      = &mocklu.MockWrapFS{}
 		dst         = "~/some/unusual/name"
 		src         = "/tmp"
-		bindOption  = []string{""} // for bind == false
+		bindOption  = []string{"", ""} // for bind == false and empty mount opts
 		expectedErr = errors.New("error")
 		err         error
 	)
@@ -112,6 +112,53 @@ func TestFSOperationsImpl_PrepareAndPerformMount_Fail(t *testing.T) {
 	wrapFS.AssertNotCalled(t, "RmDir", dst)
 }
 
+func TestFSOperationsImpl_PrepareAndPerformMount_Success_WithMountOptions(t *testing.T) {
+	var (
+		fsOps         = NewFSOperationsImpl(&command.Executor{}, logrus.New())
+		wrapFS        = &mocklu.MockWrapFS{}
+		dst           = "~/some/unusual/name"
+		src           = "/tmp"
+		mountOptions1 = []string{"noatime"}
+		mountOptions2 = []string{"noatime", "nodev"}
+		cmdOptions1   = []string{"", "-o noatime"}             // bind == false && 1 mountOpt
+		cmdOptions2   = []string{"", "-o noatime,nodev"}       // bind == false && 2 mountOpt
+		cmdOptions3   = []string{"--bind", "-o noatime,nodev"} // bind == true && 2 mountOpt
+		cmdOptions4   = []string{"--bind", ""}                 // bind == true && 0 mountOpt
+		cmdOptions5   = []string{"", ""}                       // bind == false && 0 mountOpt
+		err           error
+	)
+
+	fsOps.WrapFS = wrapFS
+	wrapFS.On("MkDir", dst).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, cmdOptions1).Return(nil).Once()
+	err = fsOps.PrepareAndPerformMount(src, dst, false, true, mountOptions1...)
+	assert.Nil(t, err)
+
+	fsOps.WrapFS = wrapFS
+	wrapFS.On("MkDir", dst).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, cmdOptions2).Return(nil).Once()
+	err = fsOps.PrepareAndPerformMount(src, dst, false, true, mountOptions2...)
+	assert.Nil(t, err)
+
+	fsOps.WrapFS = wrapFS
+	wrapFS.On("MkDir", dst).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, cmdOptions3).Return(nil).Once()
+	err = fsOps.PrepareAndPerformMount(src, dst, true, true, mountOptions2...)
+	assert.Nil(t, err)
+
+	fsOps.WrapFS = wrapFS
+	wrapFS.On("MkDir", dst).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, cmdOptions4).Return(nil).Once()
+	err = fsOps.PrepareAndPerformMount(src, dst, true, true)
+	assert.Nil(t, err)
+
+	fsOps.WrapFS = wrapFS
+	wrapFS.On("MkDir", dst).Return(nil).Once()
+	wrapFS.On("Mount", src, dst, cmdOptions5).Return(nil).Once()
+	err = fsOps.PrepareAndPerformMount(src, dst, false, true)
+	assert.Nil(t, err)
+}
+
 func TestFSOperationsImpl_MountWithCheck_Success(t *testing.T) {
 	var (
 		fsOps  = NewFSOperationsImpl(&command.Executor{}, logrus.New())
diff --git a/test/kind/small-kind.yaml b/test/kind/small-kind.yaml
new file mode 100644
index 0000000..70432b3
--- /dev/null
+++ b/test/kind/small-kind.yaml
@@ -0,0 +1,27 @@
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+# 1 control plane node and 2 workers
+nodes:
+# the control plane node config
+- role: control-plane
+# the two workers
+- role: worker
+  extraMounts:
+  - hostPath: /dev
+    containerPath: /dev
+  - hostPath: /run/udev
+    containerPath: /run/udev
+  - hostPath: /run/lvm
+    containerPath: /run/lvm
+  - hostPath: /run/lock
+    containerPath: /run/lock
+- role: worker
+  extraMounts:
+  - hostPath: /dev
+    containerPath: /dev
+  - hostPath: /run/udev
+    containerPath: /run/udev
+  - hostPath: /run/lvm
+    containerPath: /run/lvm
+  - hostPath: /run/lock
+    containerPath: /run/lock