├── CNAME ├── _config.yml ├── docs ├── CNAME ├── _config.yml └── index.md ├── .gitignore ├── go.mod ├── go.sum ├── Dockerfile ├── Makefile ├── install.yaml ├── LICENSE.md ├── test.yaml ├── install.sh ├── README.md ├── main.go └── main_test.go /CNAME: -------------------------------------------------------------------------------- 1 | k8scifsvol.morimoto.net.br -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | k8scifsvol.morimoto.net.br -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | kubernetes-cifs-volumedriver 2 | .history 3 | .tmp -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/juliohm1978/kubernetes-cifs-volumedriver 2 | 3 | go 1.13 4 | 5 | require github.com/pkg/errors v0.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.3 AS build-env 2 | ARG TARGETOS 3 | ARG TARGETARCH 4 | ENV GOOS=${TARGETOS} 5 | ENV GOARCH=${TARGETARCH} 6 | 7 | RUN apt-get update && apt-get install -y git gcc 8 | ADD . /kubernetes-cifs-volumedriver 9 | WORKDIR /kubernetes-cifs-volumedriver 10 | 11 | ## Running these in separate steps gives a better error 12 | ## output indicating which one actually failed. 13 | RUN go build -a -installsuffix cgo 14 | RUN go test 15 | 16 | FROM busybox:1.32.0 17 | 18 | ENV VENDOR=juliohm 19 | ENV DRIVER=cifs 20 | 21 | COPY --from=build-env /kubernetes-cifs-volumedriver/kubernetes-cifs-volumedriver / 22 | COPY install.sh /usr/local/bin/ 23 | RUN chmod +x /usr/local/bin/install.sh 24 | 25 | CMD ["/usr/local/bin/install.sh"] 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAGNAME=juliohm/kubernetes-cifs-volumedriver-installer 2 | VERSION=2.4 3 | DOCKER_CLI_EXPERIMENTAL=enabled 4 | PLATFORMS=linux/amd64,linux/386,linux/arm,linux/arm64,linux/ppc64le 5 | # PLATFORMS=linux/amd64 6 | 7 | build: 8 | go build -a -installsuffix cgo 9 | 10 | test: 11 | go test 12 | 13 | docker: 14 | sudo docker buildx build \ 15 | -t $(TAGNAME):$(VERSION) \ 16 | --progress plain \ 17 | --platform=$(PLATFORMS) \ 18 | . 19 | 20 | push: 21 | sudo docker buildx build \ 22 | --push \ 23 | -t $(TAGNAME):$(VERSION) \ 24 | --progress plain \ 25 | --platform=$(PLATFORMS) \ 26 | . 27 | 28 | install: 29 | kubectl apply -f install.yaml 30 | 31 | delete: 32 | kubectl delete -f install.yaml 33 | 34 | clean: 35 | rm -fr kubernetes-cifs-volumedriver 36 | -------------------------------------------------------------------------------- /install.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: juliohm-cifs-volumedriver-installer 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: juliohm-cifs-volumedriver-installer 9 | template: 10 | metadata: 11 | name: juliohm-cifs-volumedriver-installer 12 | labels: 13 | app: juliohm-cifs-volumedriver-installer 14 | spec: 15 | containers: 16 | - image: juliohm/kubernetes-cifs-volumedriver-installer:2.4 17 | name: flex-deploy 18 | imagePullPolicy: Always 19 | securityContext: 20 | privileged: true 21 | volumeMounts: 22 | - mountPath: /flexmnt 23 | name: flexvolume-mount 24 | volumes: 25 | - name: flexvolume-mount 26 | hostPath: 27 | path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Julio Henrique Morimoto 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | data: 4 | password: dXNlcjEyMw== 5 | username: cGFzczEyMw== 6 | kind: Secret 7 | metadata: 8 | name: my-secret 9 | type: juliohm/cifs 10 | --- 11 | apiVersion: v1 12 | kind: PersistentVolume 13 | metadata: 14 | name: test-volume 15 | spec: 16 | storageClassName: none 17 | capacity: 18 | storage: 1Gi 19 | flexVolume: 20 | driver: juliohm/cifs 21 | options: 22 | opts: domain=Foo 23 | server: 10.0.0.114 24 | share: /publico 25 | secretRef: 26 | name: my-secret 27 | accessModes: 28 | - ReadWriteMany 29 | --- 30 | apiVersion: v1 31 | kind: PersistentVolumeClaim 32 | metadata: 33 | name: test-claim 34 | spec: 35 | resources: 36 | requests: 37 | storage: 1Gi 38 | volumeName: test-volume 39 | storageClassName: none 40 | accessModes: 41 | - ReadWriteMany 42 | --- 43 | apiVersion: apps/v1 44 | kind: Deployment 45 | metadata: 46 | name: nginx-deployment 47 | labels: 48 | app: nginx 49 | spec: 50 | replicas: 1 51 | selector: 52 | matchLabels: 53 | app: nginx 54 | template: 55 | metadata: 56 | labels: 57 | app: nginx 58 | spec: 59 | containers: 60 | - name: nginx 61 | image: nginx 62 | ports: 63 | - containerPort: 80 64 | volumeMounts: 65 | - name: test 66 | mountPath: /dados 67 | securityContext: 68 | fsGroup: 333 69 | volumes: 70 | - name: test 71 | persistentVolumeClaim: 72 | claimName: test-claim -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | 6 | ## VENDOR and DRIVER are fixed in the Dockerfile. 7 | ## Can be defined from the container runtime. 8 | 9 | # Assuming the single driver file is located at /$DRIVER inside the DaemonSet image. 10 | 11 | driver_dir=$VENDOR${VENDOR:+"~"}${DRIVER} 12 | 13 | echo 'Installing driver '$driver_dir'/'$DRIVER 14 | 15 | if [ ! -d "/flexmnt/$driver_dir" ]; then 16 | mkdir "/flexmnt/$driver_dir" 17 | fi 18 | 19 | cp "/kubernetes-cifs-volumedriver" "/flexmnt/$driver_dir/.$DRIVER" 20 | mv -f "/flexmnt/$driver_dir/.$DRIVER" "/flexmnt/$driver_dir/$DRIVER" 21 | 22 | chmod +x "/flexmnt/$driver_dir/$DRIVER" 23 | 24 | echo ' 25 | _ _ _ _ __ _ __ 26 | (_)_ _| (_) ___ | |__ _ __ ___ / /__(_)/ _|___ 27 | | | | | | | |/ _ \| '_ \| '_ ` _ \ / / __| | |_/ __| 28 | | | |_| | | | (_) | | | | | | | | |/ / (__| | _\__ \ 29 | _/ |\__,_|_|_|\___/|_| |_|_| |_| |_/_/ \___|_|_| |___/ 30 | |__/ 31 | 32 | Driver has been installed. 33 | Make sure /flexmnt from this container mounts to Kubernetes driver directory. 34 | 35 | k8s 1.8.x 36 | /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ 37 | 38 | This path may be different in your system due to kubelet parameter --volume-plugin-dir. 39 | 40 | This driver depends on the following packages to be installed on the host: 41 | 42 | ## ubuntu 43 | apt-get install -y cifs-utils 44 | 45 | ## centos 46 | yum install -y cifs-utils 47 | 48 | This container can now be stopped and removed. 49 | 50 | ' 51 | 52 | while : ; do 53 | sleep 3600 54 | done 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes CIFS Volume Driver 2 | 3 | [![nodesource/node](https://dockeri.co/image/juliohm/kubernetes-cifs-volumedriver-installer)](https://registry.hub.docker.com/u/juliohm/kubernetes-cifs-volumedriver-installer/) 4 | 5 | ## Important note 6 | 7 | As of September 2023, personally, I have had no time to maintain this repo. It hasn't had any updates for some time now, and my current work schedule and priorities doe not allow me the time for it. 8 | 9 | Support for flexVolumes is now deprecated and will soon be removed from Kubernete's main releases. 10 | https://github.com/juliohm1978/kubernetes-cifs-volumedriver/issues/36 11 | 12 | I have no plans to implement this using the new CSI spec. Feel free to search for the best alternatives. I would like to thank the entire community for embracing this initial implementation. It's been a pleasure! 13 | 14 | ## Supported versions 15 | 16 | A simple volume driver based on [Kubernetes' Flexvolume](https://github.com/kubernetes/community/blob/master/contributors/devel/flexvolume.md) that allows Kubernetes hosts to mount CIFS volumes (samba shares) into pods and containers. 17 | 18 | It has been tested under Kubernetes versions: 19 | 20 | * 1.8.x 21 | * 1.9.x 22 | * 1.10.x 23 | * 1.11.x 24 | * 1.12.x 25 | * 1.13.x 26 | * 1.14.x 27 | * 1.15.x 28 | * 1.16.x 29 | * 1.17.x 30 | * 1.18.x 31 | 32 | ## Documentation moved go Github Pages 33 | 34 | Head over to for a complete installation guide and examples. 35 | 36 | > WARNING: The documentation for this project is no longer hosted as subdomain of juliohm.com.br. 37 | > Afer Nov/7th/2021, the domain has changed to 38 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | 6 | "bytes" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "strings" 15 | "syscall" 16 | ) 17 | 18 | const ( 19 | retStatSuccess = "Success" 20 | retStatFailure = "Failure" 21 | retStatNotSupported = "Not supported" 22 | retMsgInsufficientArgs = "Insufficient arguments" 23 | retMsgUnsupportedOperation = "Unsupported operation" 24 | retMsgInvalidMounterArgs = "Invalid mounter arguments" 25 | ) 26 | 27 | const logFileName = "/var/log/kubernetes-cifs-volumedriver.log" 28 | 29 | // returnMsg is the response given back to k8s 30 | type returnMsg struct { 31 | Status string 32 | Message string 33 | Capabilities capabilities 34 | } 35 | 36 | // Part of the repsonse that informs the driver's capabilities 37 | type capabilities struct { 38 | Attach bool 39 | FSGroup bool 40 | SupportsMetrics bool 41 | 42 | // TODO: Check if these capabilities make sense for this driver. 43 | // SELinuxRelabel bool 44 | // RequiresFSResize bool 45 | } 46 | 47 | // arguments passed by k8 to this driver 48 | type mounterArgs struct { 49 | FsGroup string `json:"kubernetes.io/mounterArgs.FsGroup"` 50 | FsGroupLegacy string `json:"kubernetes.io/fsGroup"` // k8s prior to 1.15 51 | FsType string `json:"kubernetes.io/fsType"` 52 | PodName string `json:"kubernetes.io/pod.name"` 53 | PodNamespace string `json:"kubernetes.io/pod.namespace"` 54 | PodUID string `json:"kubernetes.io/pod.uid"` 55 | PvName string `json:"kubernetes.io/pvOrVolumeName"` 56 | ReadWrite string `json:"kubernetes.io/readwrite"` 57 | ServiceAccount string `json:"kubernetes.io/serviceAccount.name"` 58 | Opts string `json:"opts"` 59 | Server string `json:"server"` 60 | Share string `json:"share"` 61 | PasswdMethod string `json:"passwdMethod"` 62 | CredentialDomain string `json:"kubernetes.io/secret/domain"` 63 | CredentialUser string `json:"kubernetes.io/secret/username"` 64 | CredentialPass string `json:"kubernetes.io/secret/password"` 65 | } 66 | 67 | func unmarshalMounterArgs(s string) (ma mounterArgs) { 68 | ma = mounterArgs{} 69 | err := json.Unmarshal([]byte(s), &ma) 70 | if err != nil { 71 | panic(fmt.Sprintf("Error interpreting mounter args: %s", err)) 72 | } 73 | if ma.CredentialDomain != "" { 74 | decoded, err := base64.StdEncoding.DecodeString(ma.CredentialDomain) 75 | if err != nil { 76 | panic(fmt.Sprintf("Error decoding credential domain: %s", err)) 77 | } 78 | ma.CredentialDomain = string(decoded) 79 | } 80 | if ma.CredentialUser != "" { 81 | decoded, err := base64.StdEncoding.DecodeString(ma.CredentialUser) 82 | if err != nil { 83 | panic(fmt.Sprintf("Error decoding credential user: %s", err)) 84 | } 85 | ma.CredentialUser = string(decoded) 86 | } 87 | if ma.CredentialPass != "" { 88 | decoded, err := base64.StdEncoding.DecodeString(ma.CredentialPass) 89 | if err != nil { 90 | panic(fmt.Sprintf("Error decoding credential password: %s", err)) 91 | } 92 | ma.CredentialPass = string(decoded) 93 | } 94 | 95 | // If we got fsGroup from the legacy json field, assume k8s prior to 1.15 96 | if ma.FsGroupLegacy != "" { 97 | ma.FsGroup = ma.FsGroupLegacy 98 | } 99 | return 100 | } 101 | 102 | func runCommand(cmd *exec.Cmd) error { 103 | var b bytes.Buffer 104 | cmd.Stdout = &b 105 | cmd.Stderr = &b 106 | 107 | if err := cmd.Start(); err != nil { 108 | return errors.Wrapf(err, "Error start cmd [cmd=%s]", cmd) 109 | } 110 | 111 | if err := cmd.Wait(); err != nil { 112 | if exiterr, ok := err.(*exec.ExitError); ok { 113 | status, ok := exiterr.Sys().(syscall.WaitStatus) 114 | if ok && status.ExitStatus() != 32 { 115 | // The program has exited with an exit code != 0 116 | // Status code 32 means not mounted 117 | return errors.Wrapf(err, "Error running cmd [cmd=%s] [response=%s]", cmd, string(b.Bytes())) 118 | } 119 | } else { 120 | return errors.Wrapf(err, "Error waiting for cmd to finish [cmd=%s]", cmd) 121 | } 122 | } 123 | return nil 124 | } 125 | 126 | func createMountCmd(cmdLineArgs []string) (cmd *exec.Cmd) { 127 | if len(cmdLineArgs) < 4 { 128 | panic(retMsgInsufficientArgs) 129 | } 130 | 131 | var mArgs mounterArgs = unmarshalMounterArgs(cmdLineArgs[3]) 132 | var optsFinal []string 133 | cmd = exec.Command("mount") 134 | cmd.Args = append(cmd.Args, "-t") 135 | cmd.Args = append(cmd.Args, "cifs") 136 | 137 | if mArgs.FsGroup != "" { 138 | optsFinal = append(optsFinal, fmt.Sprintf("uid=%s,gid=%s", mArgs.FsGroup, mArgs.FsGroup)) 139 | } 140 | if mArgs.ReadWrite != "" { 141 | optsFinal = append(optsFinal, mArgs.ReadWrite) 142 | } 143 | if mArgs.CredentialDomain != "" { 144 | optsFinal = append(optsFinal, fmt.Sprintf("domain=%s", strings.Trim(mArgs.CredentialDomain, "\n\r"))) 145 | } 146 | if mArgs.CredentialUser != "" { 147 | optsFinal = append(optsFinal, fmt.Sprintf("username=%s", strings.Trim(mArgs.CredentialUser, "\n\r"))) 148 | } 149 | if mArgs.CredentialPass != "" { 150 | if mArgs.PasswdMethod == "env" { 151 | //cmd.Env = []string{fmt.Sprintf("PASSWD=%s", strings.Trim(mArgs.CredentialPass, "\n\r"))} 152 | cmd.Env = append(os.Environ(), fmt.Sprintf("PASSWD=%s", strings.Trim(mArgs.CredentialPass, "\n\r"))) 153 | } else { 154 | optsFinal = append(optsFinal, fmt.Sprintf("password=%s", strings.Trim(mArgs.CredentialPass, "\n\r"))) 155 | } 156 | } 157 | if mArgs.Opts != "" { 158 | optsFinal = append(optsFinal, strings.Split(mArgs.Opts, ",")...) 159 | } 160 | if len(optsFinal) > 0 { 161 | cmd.Args = append(cmd.Args, "-o", strings.Join(optsFinal, ",")) 162 | } 163 | 164 | cmd.Args = append(cmd.Args, fmt.Sprintf("//%s%s", mArgs.Server, mArgs.Share)) 165 | cmd.Args = append(cmd.Args, cmdLineArgs[2]) 166 | 167 | return cmd 168 | } 169 | 170 | func createUmountCmd(cmdLineArgs []string) (cmd *exec.Cmd) { 171 | if len(cmdLineArgs) < 3 { 172 | panic(retMsgInsufficientArgs) 173 | } 174 | cmd = exec.Command("umount") 175 | cmd.Args = append(cmd.Args, cmdLineArgs[2]) 176 | return cmd 177 | } 178 | 179 | // Dettach from main, allows tests to be written for this function 180 | func driverMain(args []string) (ret returnMsg) { 181 | ret.Status = retStatSuccess 182 | 183 | defer func() { 184 | err := recover() 185 | if err != nil { 186 | ret.Status = retStatFailure 187 | ret.Message = fmt.Sprintf("Unexpected executing volume driver: %s", err) 188 | return 189 | } 190 | }() 191 | 192 | if len(args) < 2 { 193 | ret.Status = retStatFailure 194 | ret.Message = retMsgInsufficientArgs 195 | return 196 | } 197 | 198 | var operation = args[1] 199 | var err error 200 | switch operation { 201 | case "init": 202 | log.Println("Driver init") 203 | ret.Status = retStatSuccess 204 | ret.Capabilities.Attach = false // this driver does not attach any devices 205 | ret.Capabilities.FSGroup = false // avoids chown/chmod upstream in driver caller 206 | ret.Capabilities.SupportsMetrics = false // there are no metrics 207 | case "mount": 208 | cmd := createMountCmd(args) 209 | log.Println(cmd.Args) 210 | err = runCommand(cmd) 211 | if err != nil { 212 | ret.Status = retStatFailure 213 | ret.Message = fmt.Sprintf("Error: %s", err) 214 | } 215 | case "unmount": 216 | cmd := createUmountCmd(args) 217 | log.Println(cmd.Args) 218 | err = runCommand(cmd) 219 | if err != nil { 220 | ret.Status = retStatFailure 221 | ret.Message = fmt.Sprintf("Error: %s", err) 222 | } 223 | default: 224 | ret.Status = retStatNotSupported 225 | ret.Message = retMsgUnsupportedOperation + ": " + operation 226 | } 227 | return 228 | } 229 | 230 | func main() { 231 | // Log to file only if the log file already exists on disk. 232 | if _, err := os.Stat(logFileName); err == nil { 233 | logfile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 234 | if err != nil { 235 | log.Printf("WARNING: error opening file: %v", err) 236 | } 237 | log.SetOutput(logfile) 238 | } else { 239 | log.SetOutput(ioutil.Discard) 240 | } 241 | 242 | m := driverMain(os.Args) 243 | jsonString, _ := json.Marshal(m) 244 | fmt.Println(string(jsonString)) 245 | log.Println(string(jsonString)) 246 | if m.Status != retStatSuccess { 247 | os.Exit(1) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Kubernetes CIFS Volume Driver 2 | 3 | A simple volume driver based on [Kubernetes' Flexvolume](https://github.com/kubernetes/community/blob/master/contributors/devel/flexvolume.md) that allows Kubernetes hosts to mount CIFS volumes (samba shares) into pods and containers. 4 | 5 | It has been tested under Kubernetes versions: 6 | 7 | * 1.8.x 8 | * 1.9.x 9 | * 1.10.x 10 | * 1.11.x 11 | * 1.12.x 12 | * 1.13.x 13 | * 1.14.x 14 | * 1.15.x 15 | * 1.16.x 16 | * 1.17.x 17 | * 1.18.x 18 | * 1.20.x 19 | 20 | > NOTE: Starting at v2.0, the driver has been fully reimplemented using [Go](https://golang.org/). As a full-fledged programming language, it provides a more robust solution and better error handling. 21 | > 22 | > Because Go can handle Json objects natively, the `jq` dependency is no longer necessary. The driver still relies on the `mount.cifs` binary, however, which is used to issue mount commands in the host OS. Aside from a different code base, all features should work the same as expected. 23 | > 24 | > The last implementation using Bash was v0.6. You can visit the tag to review the documentation for that release. 25 | > 26 | 27 | ## Pre-requisites 28 | 29 | On your Kubernetes nodes, install `cifs-utils` because the host itself will do the mounting. 30 | 31 | For Debian-based distributions: 32 | 33 | ```bash 34 | sudo apt-get install -y cifs-utils 35 | ``` 36 | 37 | For RedHat distributions: 38 | 39 | ```bash 40 | yum -y install cifs-utils 41 | ``` 42 | 43 | If you are planning to mount DFS shares, you also need `keyutils` ([Ubuntu](https://bugs.launchpad.net/ubuntu/+source/cifs-utils/+bug/1772148), [RedHat](https://access.redhat.com/solutions/45070)). 44 | 45 | ## Manual Installation 46 | 47 | Flexvolumes are very straight forward. The driver needs to be copied into a special volume plugin directory of your Kubernetes cluster. 48 | 49 | For manual installation, you will need to compile the code to create the binary executable. If you have Go installed, it should be easy as `make`. 50 | 51 | ```bash 52 | go get github.com/juliohm1978/kubernetes-cifs-volumedriver 53 | cd $GOPATH/src/github.com/juliohm1978/kubernetes-cifs-volumedriver 54 | 55 | make 56 | 57 | ## if you want to be sure, run the test suite 58 | make test 59 | ``` 60 | 61 | That should create the binary `kubernetes-cifs-volumedriver` that you can copy to your Kubernetes nodes. 62 | 63 | ```bash 64 | ## as root in all kubernetes nodes 65 | mkdir -p /usr/libexec/kubernetes/kubelet-plugins/volume/exec/juliohm~cifs 66 | cp -vr kubernetes-cifs-volumedriver /usr/libexec/kubernetes/kubelet-plugins/volume/exec/juliohm~cifs/cifs 67 | chmod +x /usr/libexec/kubernetes/kubelet-plugins/volume/exec/juliohm~cifs/* 68 | ``` 69 | 70 | This procedure should be simple enough for testing purposes, so feel free to automate this in any way. Once the binary is copied and marked as executable, Kubelet should automatically pick it up and it should be working. 71 | 72 | ### Building a Docker image 73 | 74 | Starting at `v2.3`, the docker build for this project supports multiple architectures. In order to build the image locally without using multiple build nodes for different platforms, you need to install qemu dependencies and make sure you have Docker 19.03+ in order to use BuildKit. 75 | 76 | ``` 77 | ## For Ubuntu 78 | sudo apt-get install qemu-user-static 79 | ``` 80 | 81 | Configure a local builder instance that uses the `docker-container` driver. 82 | 83 | ``` 84 | docker buildx create --name mybuilder --driver docker-container --use 85 | ``` 86 | 87 | ```bash 88 | make docker 89 | ``` 90 | 91 | Throughout this build, Docker will spawn a number of qemu simulators, for each architecture not supported by the host kernel. This will consume more resources than a usual docker build. 92 | 93 | ## DaemonSet Installation 94 | 95 | When dealing with a large cluster, manually copying the driver to all hosts becomes inhuman. As proposed in [Flexvolume's documentation](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/flexvolume-deployment.md#recommended-driver-deployment-method), the recommended driver deployment method is to have a DaemonSet install the driver cluster-wide automatically. 96 | 97 | A Docker image [juliohm/kubernetes-cifs-volumedriver-intaller](https://hub.docker.com/r/juliohm/kubernetes-cifs-volumedriver-installer/) is available for this purpose, which can be deployed into a Kubernetes cluster using the `install.yaml` from this repository. The image is built `FROM busybox`, so the it's essentially very small. 98 | 99 | The installer image allows you to install without the need to compile the project. Deploying the volume driver should be as easy as `make install`: 100 | 101 | ```bash 102 | git clone https://github.com/juliohm1978/kubernetes-cifs-volumedriver.git 103 | cd kubernetes-cifs-volumedriver 104 | 105 | make install 106 | ``` 107 | 108 | The `install` target uses kubectl to create a privileged DaemonSet with pods that mount the host directory `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/` for installation. Check the output from the deployed containers to make sure it did not produce any errors. Crashing pods mean something went wrong. 109 | 110 | > *NOTE*: This deployment does NOT install host dependencies, which still needs to be done manually on all hosts. See previous chapter *Pre-requisites*. 111 | 112 | If you need to tweak or customize the installation, you can modify the `install.yaml` directly. 113 | 114 | Installing is a one time job. So, once you have verified that it's completed, the DaemonSet can be safely removed. 115 | 116 | ```bash 117 | make delete 118 | ``` 119 | 120 | ## The Volume Plugin Directory 121 | 122 | As of today with Kubernetes v1.16, the kubelet's default directory for volume plugins is `/usr/libexec/kubernetes/kubelet-plugins/volume/exec/`. This could be different if your installation changed this directory using the `--volume-plugin-dir` parameter. 123 | 124 | A known example of this change is the installation provided by [Kubespray](https://github.com/kubernetes-incubator/kubespray), which at version v2.4.0 uses `/var/lib/kubelet/volume-plugins`. 125 | 126 | Please, review the [kubelet command line parameters](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/) (namely `--volume-plugin-dir`) and make sure it matches the directory where the driver will be installed. 127 | 128 | You can modify `install.yaml` and change the field `spec.template.spec.volumes.hostPath.path` to the path used by your Kubernetes installation. 129 | 130 | ## Customizing the Vendor/Driver name 131 | 132 | By default, the driver installation path is `$KUBELET_PLUGIN_DIRECTORY/juliohm~cifs/cifs`. 133 | 134 | For some installations, you may need to change the vendor+driver name. Starting at v2.0, you can customize the vendor name/directory for your installation by tweaking `install.yaml` and defining `VENDOR` and `DRIVER` environment variables. 135 | 136 | ```yaml 137 | 138 | ## snippet ## 139 | 140 | containers: 141 | - image: juliohm/kubernetes-cifs-volumedriver-installer:2.4 142 | env: 143 | - name: VENDOR 144 | value: mycompany 145 | - name: DRIVER 146 | value: mycifs 147 | 148 | ## snippet ## 149 | 150 | ``` 151 | 152 | The example above will install the driver in the path `$KUBELET_PLUGIN_DIRECTORY/mycompany~mycifs/mycifs`. For the most part, changig the `VENDOR` variable should be enough to make your installation unique to your needs. 153 | 154 | ## Example of PersistentVolume 155 | 156 | The following is an example of PersistentVolume that uses the volume driver. 157 | 158 | ```yaml 159 | apiVersion: v1 160 | kind: PersistentVolume 161 | metadata: 162 | name: mycifspv 163 | spec: 164 | capacity: 165 | storage: 1Gi 166 | flexVolume: 167 | driver: juliohm/cifs 168 | options: 169 | opts: sec=ntlm,uid=1000 170 | server: my-cifs-host 171 | share: /MySharedDirectory 172 | secretRef: 173 | name: my-secret 174 | accessModes: 175 | - ReadWriteMany 176 | ``` 177 | 178 | Credentials are passed using a Secret, which can be declared as follows. 179 | 180 | ```yaml 181 | apiVersion: v1 182 | data: 183 | password: ### 184 | username: ### 185 | kind: Secret 186 | metadata: 187 | name: my-secret 188 | type: juliohm/cifs 189 | ``` 190 | 191 | ## Passwords with comma 192 | 193 | In a perfectly reasonable scenario, passwords may be required to have special characters. In general, this driver no longer depends on shell variable parsing, so most special characters will work as expected. 194 | 195 | An exception case can be made for commas, which is closely related to how `mount` implements its own argument parsing. The official workaround suggested is to use a `PASSWD` environment variable to store the password temporarily. 196 | 197 | https://linux.die.net/man/8/mount.cifs 198 | 199 | This driver includes an option to use that method as an alternative. If your password has commas, try enabling it with the option `passwdMethod: env`. 200 | 201 | ```yaml 202 | apiVersion: v1 203 | kind: PersistentVolume 204 | metadata: 205 | name: mycifspv 206 | spec: 207 | capacity: 208 | storage: 1Gi 209 | flexVolume: 210 | driver: juliohm/cifs 211 | options: 212 | opts: sec=ntlm,uid=1000 213 | server: my-cifs-host 214 | share: /MySharedDirectory 215 | passwdMethod: env 216 | secretRef: 217 | name: my-secret 218 | accessModes: 219 | - ReadWriteMany 220 | ``` 221 | 222 | This causes the driver to use the `PASSWD` environment variable, instead of the usual `password=***` option in the command line. 223 | 224 | ## Using `securityContext` to inform uid/gid parameters 225 | 226 | Starting at version 0.5, the driver will also accept values coming from the Pod's `securityContext`. 227 | 228 | For example, consider the following Deployment: 229 | 230 | ```yaml 231 | apiVersion: apps/v1 232 | kind: Deployment 233 | metadata: 234 | name: nginx-deployment 235 | labels: 236 | app: nginx 237 | spec: 238 | replicas: 1 239 | selector: 240 | matchLabels: 241 | app: nginx 242 | template: 243 | metadata: 244 | labels: 245 | app: nginx 246 | spec: 247 | containers: 248 | - name: nginx 249 | image: nginx 250 | ports: 251 | - containerPort: 80 252 | volumeMounts: 253 | - name: test 254 | mountPath: /dados 255 | securityContext: 256 | runAsUser: 33 257 | runAsGroup: 33 258 | fsGroup: 33 259 | volumes: 260 | - name: test 261 | persistentVolumeClaim: 262 | claimName: test-claim 263 | ``` 264 | 265 | ... which defines a `securityContext`. 266 | 267 | ```yaml 268 | securityContext: 269 | runAsUser: 33 270 | runAsGroup: 33 271 | fsGroup: 33 272 | ``` 273 | 274 | The value of `fsGroup` is passed to the volume driver, but previous versions would ignore that. It is now used to construct `uid` and `gid` parameters for the mount command. 275 | 276 | If you are using versions older than v0.5, you can still workaround by including these values in the `spec.flexVolume.options.opts` field of the PersistentVolume. 277 | 278 | ```yaml 279 | ## PV spec 280 | spec: 281 | flexVolume: 282 | driver: juliohm/cifs 283 | options: 284 | opts: domain=Foo,uid=33,gid=33 285 | ``` 286 | 287 | ## Troubleshooting and Known Issues 288 | 289 | Because the driver is fundamentally a wrapper to `mount.cifs`, understanding what's happening at runtime can be challenging. 290 | 291 | Remember to install the dependency: `cifs-utils`. Whatever your host OS should be, the driver attempts to issue `mount -t cifs...` to mount the volume. It should be installed on every node of the cluster. 292 | 293 | Pay attention to the secret's `type` field, which **MUST** match the volume driver name. Otherwise, the secret values will not be passed to the driver. 294 | 295 | If your Pod is stuck trying to mount a volume, check the events from the Kubernetes API. 296 | 297 | ```shell 298 | kubectl describe pod POD_NAME 299 | ``` 300 | 301 | Events at the bottom of the Pod'd description will show errors collected by the driver while trying to mount the volume. So, you might see somethingn like this: 302 | 303 | ```text 304 | Events: 305 | Type Reason Age From Message 306 | ---- ------ ---- ---- ------- 307 | Normal Scheduled 34s default-scheduler Successfully assigned default/nginx-deployment-58fc77b8db-hf47f to minikube 308 | Warning FailedMount 18s (x6 over 34s) kubelet, minikube MountVolume.SetUp failed for volume "test-volume" : Couldn't get secret default/my-secret err: secrets "my-secret" not found 309 | Warning FailedMount 2s kubelet, minikube MountVolume.SetUp failed for volume "test-volume" : mount command failed, status: Failure, reason: Error: exit status 32 310 | ``` 311 | 312 | ... where `Error: exit status 32` is the output of the mount command. 313 | 314 | ### Enabling Logs 315 | 316 | Starting at v2.0, the Go implementation provides a basic logging mechanism. This could help you even further to understand why your volume fails to mount. 317 | 318 | The driver attempts to write log messages to `/var/log/kubernetes-cifs-volumedriver.log`, but **only if that file already exists on disk and is writable**. Because log messages show all arguments issued to the `mount` command, password and secrets can be exposed. For that reason, logging is disabled by default. To enable, simply create the log file and wait for messages to come in. 319 | 320 | ```shell 321 | ## On the Kubernetes node where the Pod is scheduled 322 | touch /var/log/kubernetes-cifs-volumedriver.log 323 | tail -F /var/log/kubernetes-cifs-volumedriver.log 324 | ``` 325 | 326 | Once you no longer need to keep log messages, you can disable it by simply removing the log file. 327 | 328 | ```shell 329 | rm /var/log/kubernetes-cifs-volumedriver.log 330 | ``` 331 | 332 | Log messages will look similar to this: 333 | 334 | ```text 335 | # tail -F kubernetes-cifs-volumedriver.log 336 | 2019/11/17 04:46:05 [mount -t cifs -o,uid=333,gid=333,rw,username=pass123,password=user123,domain=Foo //10.0.0.114/publico /var/lib/kubelet/pods/7c92dd1d-5303-479e-8ff2-16713ae655c9/volumes/juliohm~cifs/test-volume] 337 | 2019/11/17 04:46:05 {"Status":"Failure","Message":"Error: exit status 32","Capabilities":{"Attach":false}} 338 | ``` 339 | 340 | Note that the complete `mount` command is on display, along with the response given to the Kubernetes API. That should give you a clear idea of what the driver is trying to do, and possibly some insight into the root cause of the problem. 341 | 342 | ### Kubelet Logs 343 | 344 | While diagnosing issues, you might also want to checkout the output of the `kubelet` daemon. It rus on every node, in the host OS and is responsible for creating and destroying pods/containers. 345 | 346 | The location for its log file can vary, depending on how you provisioned your cluster. For Ubuntu, `kubelet` is usually installed as system service. In that case, you can use `journalctl`. 347 | 348 | ```shell 349 | journalctl -f -u kubelet 350 | ``` 351 | 352 | The output of `kubelet` may also give you clues and relevant error messages. 353 | 354 | ### Passwords and Secrets 355 | 356 | A common option is to store mount credentials in a [Kubernetes Secret](https://kubernetes.io/docs/concepts/configuration/secret) object. When creating a Secret, the contents must be base 64 encoded. One of the most common ways is to use well known Linux tools, like `echo` and `base64`. 357 | 358 | While convenient, you must be careful not to accidentally include a **new line character** at the end of your password. If hidden in the Secret, it will prevent your volume from being mounted, failing with permissions errors. 359 | 360 | ```bash 361 | ## Watch out for hidden new line chars 362 | > echo hello | base64 363 | aGVsbG8K 364 | 365 | ## This is the correct way to encode your password 366 | > echo -n hello | base64 367 | aGVsbG8= 368 | ``` 369 | 370 | By default, `echo` includes a new line character at the end of its output. To avoid that, you must use the `-n` flag. 371 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // Running without arguments should return an error message. 11 | func TestMainWithoutArgs(t *testing.T) { 12 | args := []string{} 13 | msg := driverMain(args) 14 | if msg.Status != retStatFailure { 15 | t.Error() 16 | } 17 | if msg.Message != retMsgInsufficientArgs { 18 | t.Error() 19 | } 20 | 21 | args = []string{"/path/to/binary"} 22 | msg = driverMain(args) 23 | if msg.Status != retStatFailure { 24 | t.Error() 25 | } 26 | if msg.Message != retMsgInsufficientArgs { 27 | t.Error() 28 | } 29 | } 30 | 31 | // There is nothing to init at this point. Should return a sucess message. 32 | func TestInit(t *testing.T) { 33 | args := []string{"/path/to/binary", "init"} 34 | msg := driverMain(args) 35 | if msg.Status != retStatSuccess { 36 | t.Error() 37 | } 38 | if msg.Capabilities.Attach { 39 | t.Error() 40 | } 41 | if msg.Capabilities.FSGroup { 42 | t.Error() 43 | } 44 | if msg.Capabilities.SupportsMetrics { 45 | t.Error() 46 | } 47 | } 48 | 49 | func TestUnsupportedOperation(t *testing.T) { 50 | args := []string{"/path/to/binary", "i_do_not_exist"} 51 | msg := driverMain(args) 52 | if msg.Status != retStatNotSupported { 53 | t.Error() 54 | } 55 | if !strings.HasPrefix(msg.Message, retMsgUnsupportedOperation) { 56 | t.Error() 57 | } 58 | } 59 | 60 | func TestUnmountCmd(t *testing.T) { 61 | 62 | args := []string{"/path/to/binary", "unmount", "/mnt/point"} 63 | mountCmd := createUmountCmd(args) 64 | if mountCmd == nil { 65 | t.Error() 66 | } 67 | 68 | if mountCmd.Args[0] != "umount" { 69 | t.Error() 70 | } 71 | if mountCmd.Args[1] != "/mnt/point" { 72 | t.Error() 73 | } 74 | } 75 | 76 | // Teste with all possible arguments 77 | func TestMountCmdComplete(t *testing.T) { 78 | 79 | jsonArgs := `{ 80 | "kubernetes.io/mounterArgs.FsGroup": "33", 81 | "kubernetes.io/fsType": "", 82 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 83 | "kubernetes.io/pod.namespace": "default", 84 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 85 | "kubernetes.io/pvOrVolumeName": "test-volume", 86 | "kubernetes.io/readwrite": "rw", 87 | "kubernetes.io/serviceAccount.name": "default", 88 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 89 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 90 | "kubernetes.io/secret/password": "cGFzczEyMw==", 91 | "opts": "domain=Foo", 92 | "server": "fooserver123", 93 | "share": "/test" 94 | }` 95 | 96 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 97 | mountCmd := createMountCmd(args) 98 | if mountCmd == nil { 99 | t.Error("Mount command wasn't created") 100 | } 101 | 102 | expected := []string{ 103 | "mount", 104 | "-t", 105 | "cifs", 106 | "-o", 107 | "uid=33,gid=33,rw,domain=domain123,username=user123,password=pass123,domain=Foo", 108 | "//fooserver123/test", 109 | "/mnt/point", 110 | } 111 | 112 | if len(mountCmd.Args) != len(expected) { 113 | t.Errorf("TestMountCmdComplete len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 114 | } 115 | 116 | for idx := range expected { 117 | if mountCmd.Args[idx] != expected[idx] { 118 | t.Errorf("TestMountCmdComplete[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 119 | } 120 | } 121 | 122 | } 123 | 124 | // Simplest test, without any of: 125 | // * fsGroup 126 | // * Opts 127 | // * Credentials 128 | func TestMountCmdSimplest(t *testing.T) { 129 | 130 | jsonArgs := `{ 131 | "kubernetes.io/fsType": "", 132 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 133 | "kubernetes.io/pod.namespace": "default", 134 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 135 | "kubernetes.io/pvOrVolumeName": "test-volume", 136 | "kubernetes.io/serviceAccount.name": "default", 137 | "server": "fooserver123", 138 | "share": "/test" 139 | }` 140 | 141 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 142 | mountCmd := createMountCmd(args) 143 | if mountCmd == nil { 144 | t.Error("Mount command wasn't created") 145 | } 146 | 147 | expected := []string{ 148 | "mount", 149 | "-t", 150 | "cifs", 151 | "//fooserver123/test", 152 | "/mnt/point", 153 | } 154 | if len(mountCmd.Args) != len(expected) { 155 | t.Errorf("TestMountCmdSimplest len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 156 | } 157 | 158 | for idx := range expected { 159 | if mountCmd.Args[idx] != expected[idx] { 160 | t.Errorf("TestMountCmdSimplest[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 161 | } 162 | } 163 | } 164 | 165 | func TestMountCmdWithoutCredentials(t *testing.T) { 166 | 167 | jsonArgs := `{ 168 | "kubernetes.io/mounterArgs.FsGroup": "33", 169 | "kubernetes.io/fsType": "", 170 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 171 | "kubernetes.io/pod.namespace": "default", 172 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 173 | "kubernetes.io/pvOrVolumeName": "test-volume", 174 | "kubernetes.io/readwrite": "rw", 175 | "kubernetes.io/serviceAccount.name": "default", 176 | "opts": "domain=Foo", 177 | "server": "fooserver123", 178 | "share": "/test" 179 | }` 180 | 181 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 182 | mountCmd := createMountCmd(args) 183 | if mountCmd == nil { 184 | t.Error("Mount command wasn't created") 185 | } 186 | 187 | expected := []string{ 188 | "mount", 189 | "-t", 190 | "cifs", 191 | "-o", 192 | "uid=33,gid=33,rw,domain=Foo", 193 | "//fooserver123/test", 194 | "/mnt/point", 195 | } 196 | if len(mountCmd.Args) != len(expected) { 197 | t.Errorf("TestMountCmdWithoutCredentials len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 198 | } 199 | 200 | for idx := range expected { 201 | if mountCmd.Args[idx] != expected[idx] { 202 | t.Errorf("TestMountCmdWithoutCredentials[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 203 | } 204 | } 205 | } 206 | 207 | func TestMountCmdFsGroupLegacy(t *testing.T) { 208 | 209 | jsonArgs := `{ 210 | "kubernetes.io/fsGroup": "33", 211 | "kubernetes.io/fsType": "", 212 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 213 | "kubernetes.io/pod.namespace": "default", 214 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 215 | "kubernetes.io/pvOrVolumeName": "test-volume", 216 | "kubernetes.io/readwrite": "rw", 217 | "kubernetes.io/serviceAccount.name": "default", 218 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 219 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 220 | "kubernetes.io/secret/password": "cGFzczEyMw==", 221 | "opts": "domain=Foo", 222 | "server": "fooserver123", 223 | "share": "/test" 224 | }` 225 | 226 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 227 | mountCmd := createMountCmd(args) 228 | if mountCmd == nil { 229 | t.Error("Mount command wasn't created") 230 | } 231 | 232 | expected := []string{ 233 | "mount", 234 | "-t", 235 | "cifs", 236 | "-o", 237 | "uid=33,gid=33,rw,domain=domain123,username=user123,password=pass123,domain=Foo", 238 | "//fooserver123/test", 239 | "/mnt/point", 240 | } 241 | if len(mountCmd.Args) != len(expected) { 242 | t.Errorf("TestMountCmdWithoutCredentials len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 243 | } 244 | 245 | for idx := range expected { 246 | if mountCmd.Args[idx] != expected[idx] { 247 | t.Errorf("TestMountCmdWithoutCredentials[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 248 | } 249 | } 250 | } 251 | 252 | func TestMountCmdWithoutFsGroup(t *testing.T) { 253 | 254 | jsonArgs := `{ 255 | "kubernetes.io/fsType": "", 256 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 257 | "kubernetes.io/pod.namespace": "default", 258 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 259 | "kubernetes.io/pvOrVolumeName": "test-volume", 260 | "kubernetes.io/readwrite": "rw", 261 | "kubernetes.io/serviceAccount.name": "default", 262 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 263 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 264 | "kubernetes.io/secret/password": "cGFzczEyMw==", 265 | "opts": "domain=Foo", 266 | "server": "fooserver123", 267 | "share": "/test" 268 | }` 269 | 270 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 271 | mountCmd := createMountCmd(args) 272 | if mountCmd == nil { 273 | t.Error("Mount command wasn't created") 274 | } 275 | 276 | expected := []string{ 277 | "mount", 278 | "-t", 279 | "cifs", 280 | "-o", 281 | "rw,domain=domain123,username=user123,password=pass123,domain=Foo", 282 | "//fooserver123/test", 283 | "/mnt/point", 284 | } 285 | 286 | if len(mountCmd.Args) != len(expected) { 287 | t.Errorf("TestMountCmdWithoutFsGroup len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 288 | } 289 | 290 | for idx := range expected { 291 | if mountCmd.Args[idx] != expected[idx] { 292 | t.Errorf("TestMountCmdWithoutFsGroup[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 293 | } 294 | } 295 | } 296 | 297 | func TestMountCmdInvalidCredendialDomain(t *testing.T) { 298 | 299 | // recover from panic, which is a good sign here 300 | defer func() { 301 | recover() 302 | }() 303 | 304 | jsonArgs := `{ 305 | "kubernetes.io/mounterArgs.FsGroup": "33", 306 | "kubernetes.io/fsType": "", 307 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 308 | "kubernetes.io/pod.namespace": "default", 309 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 310 | "kubernetes.io/pvOrVolumeName": "test-volume", 311 | "kubernetes.io/readwrite": "rw", 312 | "kubernetes.io/serviceAccount.name": "default", 313 | "kubernetes.io/secret/domain": "INVALID_BASE64", 314 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 315 | "kubernetes.io/secret/password": "cGFzczEyMw==", 316 | "opts": "domain=Foo", 317 | "server": "fooserver123", 318 | "share": "/test" 319 | }` 320 | 321 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 322 | createMountCmd(args) 323 | t.Error("Invalid base64 did not cause panic") 324 | } 325 | 326 | func TestMountCmdInvalidCredendialUser(t *testing.T) { 327 | 328 | // recover from panic, which is a good sign here 329 | defer func() { 330 | recover() 331 | }() 332 | 333 | jsonArgs := `{ 334 | "kubernetes.io/mounterArgs.FsGroup": "33", 335 | "kubernetes.io/fsType": "", 336 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 337 | "kubernetes.io/pod.namespace": "default", 338 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 339 | "kubernetes.io/pvOrVolumeName": "test-volume", 340 | "kubernetes.io/readwrite": "rw", 341 | "kubernetes.io/serviceAccount.name": "default", 342 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 343 | "kubernetes.io/secret/username": "INVALID_BASE64", 344 | "kubernetes.io/secret/password": "cGFzczEyMw==", 345 | "opts": "domain=Foo", 346 | "server": "fooserver123", 347 | "share": "/test" 348 | }` 349 | 350 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 351 | createMountCmd(args) 352 | t.Error("Invalid base64 did not cause panic") 353 | } 354 | 355 | func TestMountCmdInvalidCredendialPassword(t *testing.T) { 356 | 357 | // recover from panic, which is a good sign here 358 | defer func() { 359 | recover() 360 | }() 361 | 362 | jsonArgs := `{ 363 | "kubernetes.io/mounterArgs.FsGroup": "33", 364 | "kubernetes.io/fsType": "", 365 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 366 | "kubernetes.io/pod.namespace": "default", 367 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 368 | "kubernetes.io/pvOrVolumeName": "test-volume", 369 | "kubernetes.io/readwrite": "rw", 370 | "kubernetes.io/serviceAccount.name": "default", 371 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 372 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 373 | "kubernetes.io/secret/password": "INVALID_BASE64", 374 | "opts": "domain=Foo", 375 | "server": "fooserver123", 376 | "share": "/test" 377 | }` 378 | 379 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 380 | createMountCmd(args) 381 | t.Error("Invalid base64 did not cause panic") 382 | } 383 | 384 | func TestMountCmdWithoutReadWrite(t *testing.T) { 385 | 386 | jsonArgs := `{ 387 | "kubernetes.io/mounterArgs.FsGroup": "33", 388 | "kubernetes.io/fsType": "", 389 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 390 | "kubernetes.io/pod.namespace": "default", 391 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 392 | "kubernetes.io/pvOrVolumeName": "test-volume", 393 | "kubernetes.io/serviceAccount.name": "default", 394 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 395 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 396 | "kubernetes.io/secret/password": "cGFzczEyMw==", 397 | "opts": "domain=Foo", 398 | "server": "fooserver123", 399 | "share": "/test" 400 | }` 401 | 402 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 403 | mountCmd := createMountCmd(args) 404 | if mountCmd == nil { 405 | t.Error("Mount command wasn't created") 406 | } 407 | 408 | expected := []string{ 409 | "mount", 410 | "-t", 411 | "cifs", 412 | "-o", 413 | "uid=33,gid=33,domain=domain123,username=user123,password=pass123,domain=Foo", 414 | "//fooserver123/test", 415 | "/mnt/point", 416 | } 417 | 418 | if len(mountCmd.Args) != len(expected) { 419 | t.Errorf("TestMountCmdWithoutReadWrite len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 420 | } 421 | 422 | for idx := range expected { 423 | if mountCmd.Args[idx] != expected[idx] { 424 | t.Errorf("TestMountCmdWithoutReadWrite[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 425 | } 426 | } 427 | } 428 | 429 | func TestMountCmdNoCredentialsAndNoOpts(t *testing.T) { 430 | 431 | jsonArgs := `{ 432 | "kubernetes.io/mounterArgs.FsGroup": "33", 433 | "kubernetes.io/fsType": "", 434 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 435 | "kubernetes.io/pod.namespace": "default", 436 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 437 | "kubernetes.io/pvOrVolumeName": "test-volume", 438 | "kubernetes.io/readwrite": "rw", 439 | "kubernetes.io/serviceAccount.name": "default", 440 | "server": "fooserver123", 441 | "share": "/test" 442 | }` 443 | 444 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 445 | mountCmd := createMountCmd(args) 446 | if mountCmd == nil { 447 | t.Error("Mount command wasn't created") 448 | } 449 | 450 | expected := []string{ 451 | "mount", 452 | "-t", 453 | "cifs", 454 | "-o", 455 | "uid=33,gid=33,rw", 456 | "//fooserver123/test", 457 | "/mnt/point", 458 | } 459 | 460 | if len(mountCmd.Args) != len(expected) { 461 | t.Errorf("TestMountCmdNoCredentialsAndNoOpts len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 462 | } 463 | 464 | for idx := range expected { 465 | if mountCmd.Args[idx] != expected[idx] { 466 | t.Errorf("TestMountCmdNoCredentialsAndNoOpts[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 467 | } 468 | } 469 | } 470 | 471 | func TestMountCmdNoReadWrite(t *testing.T) { 472 | 473 | jsonArgs := `{ 474 | "kubernetes.io/mounterArgs.FsGroup": "33", 475 | "kubernetes.io/fsType": "", 476 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 477 | "kubernetes.io/pod.namespace": "default", 478 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 479 | "kubernetes.io/pvOrVolumeName": "test-volume", 480 | "kubernetes.io/serviceAccount.name": "default", 481 | "kubernetes.io/secret/domain": "ZG9tYWluMTIz", 482 | "kubernetes.io/secret/username": "dXNlcjEyMw==", 483 | "kubernetes.io/secret/password": "cGFzczEyMw==", 484 | "opts": "domain=Foo", 485 | "server": "fooserver123", 486 | "share": "/test" 487 | }` 488 | 489 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 490 | mountCmd := createMountCmd(args) 491 | if mountCmd == nil { 492 | t.Error("Mount command wasn't created") 493 | } 494 | 495 | expected := []string{ 496 | "mount", 497 | "-t", 498 | "cifs", 499 | "-o", 500 | "uid=33,gid=33,domain=domain123,username=user123,password=pass123,domain=Foo", 501 | "//fooserver123/test", 502 | "/mnt/point", 503 | } 504 | 505 | if len(mountCmd.Args) != len(expected) { 506 | t.Errorf("TestMountCmdNoReadWrite len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 507 | } 508 | 509 | for idx := range expected { 510 | if mountCmd.Args[idx] != expected[idx] { 511 | t.Errorf("TestMountCmdNoReadWrite[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 512 | } 513 | } 514 | } 515 | 516 | func TestMountCmdNewline(t *testing.T) { 517 | 518 | jsonArgs := fmt.Sprintf(`{ 519 | "kubernetes.io/mounterArgs.FsGroup": "33", 520 | "kubernetes.io/fsType": "", 521 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 522 | "kubernetes.io/pod.namespace": "default", 523 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 524 | "kubernetes.io/pvOrVolumeName": "test-volume", 525 | "kubernetes.io/serviceAccount.name": "default", 526 | "kubernetes.io/secret/domain": "%s", 527 | "kubernetes.io/secret/username": "%s", 528 | "kubernetes.io/secret/password": "%s", 529 | "server": "fooserver123", 530 | "share": "/test" 531 | }`, 532 | base64.StdEncoding.EncodeToString([]byte("domain123\n")), 533 | base64.StdEncoding.EncodeToString([]byte("user123\n")), 534 | base64.StdEncoding.EncodeToString([]byte("pass123\n")), 535 | ) 536 | 537 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 538 | mountCmd := createMountCmd(args) 539 | if mountCmd == nil { 540 | t.Error("Mount command wasn't created") 541 | } 542 | 543 | expected := []string{ 544 | "mount", 545 | "-t", 546 | "cifs", 547 | "-o", 548 | "uid=33,gid=33,domain=domain123,username=user123,password=pass123", 549 | "//fooserver123/test", 550 | "/mnt/point", 551 | } 552 | 553 | if len(mountCmd.Args) != len(expected) { 554 | t.Errorf("TestMountCmdNoReadWrite len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 555 | } 556 | 557 | for idx := range expected { 558 | if mountCmd.Args[idx] != expected[idx] { 559 | t.Errorf("TestMountCmdNoReadWrite[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 560 | } 561 | } 562 | } 563 | 564 | func TestMountCmdReturn(t *testing.T) { 565 | 566 | jsonArgs := fmt.Sprintf(`{ 567 | "kubernetes.io/mounterArgs.FsGroup": "33", 568 | "kubernetes.io/fsType": "", 569 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 570 | "kubernetes.io/pod.namespace": "default", 571 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 572 | "kubernetes.io/pvOrVolumeName": "test-volume", 573 | "kubernetes.io/serviceAccount.name": "default", 574 | "kubernetes.io/secret/domain": "%s", 575 | "kubernetes.io/secret/username": "%s", 576 | "kubernetes.io/secret/password": "%s", 577 | "server": "fooserver123", 578 | "share": "/test" 579 | }`, 580 | base64.StdEncoding.EncodeToString([]byte("domain123\n\r")), 581 | base64.StdEncoding.EncodeToString([]byte("user123\n\r")), 582 | base64.StdEncoding.EncodeToString([]byte("pass123\n\r")), 583 | ) 584 | 585 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 586 | mountCmd := createMountCmd(args) 587 | if mountCmd == nil { 588 | t.Error("Mount command wasn't created") 589 | } 590 | 591 | expected := []string{ 592 | "mount", 593 | "-t", 594 | "cifs", 595 | "-o", 596 | "uid=33,gid=33,domain=domain123,username=user123,password=pass123", 597 | "//fooserver123/test", 598 | "/mnt/point", 599 | } 600 | 601 | if len(mountCmd.Args) != len(expected) { 602 | t.Errorf("TestMountCmdNoReadWrite len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 603 | } 604 | 605 | for idx := range expected { 606 | if mountCmd.Args[idx] != expected[idx] { 607 | t.Errorf("TestMountCmdNoReadWrite[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 608 | } 609 | } 610 | } 611 | 612 | func TestPassEnvOption(t *testing.T) { 613 | 614 | jsonArgs := fmt.Sprintf(`{ 615 | "kubernetes.io/mounterArgs.FsGroup": "33", 616 | "kubernetes.io/fsType": "", 617 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 618 | "kubernetes.io/pod.namespace": "default", 619 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 620 | "kubernetes.io/pvOrVolumeName": "test-volume", 621 | "kubernetes.io/serviceAccount.name": "default", 622 | "kubernetes.io/secret/domain": "%s", 623 | "kubernetes.io/secret/username": "%s", 624 | "kubernetes.io/secret/password": "%s", 625 | "server": "fooserver123", 626 | "share": "/test", 627 | "passwdMethod": "env" 628 | }`, 629 | base64.StdEncoding.EncodeToString([]byte("domain123\n\r")), 630 | base64.StdEncoding.EncodeToString([]byte("user123\n\r")), 631 | base64.StdEncoding.EncodeToString([]byte("pass123\n\r")), 632 | ) 633 | 634 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 635 | mountCmd := createMountCmd(args) 636 | if mountCmd == nil { 637 | t.Error("Mount command wasn't created") 638 | } 639 | 640 | expected := []string{ 641 | "mount", 642 | "-t", 643 | "cifs", 644 | "-o", 645 | "uid=33,gid=33,domain=domain123,username=user123", 646 | "//fooserver123/test", 647 | "/mnt/point", 648 | } 649 | 650 | if len(mountCmd.Env) < 1 { 651 | t.Error("TestPassEnvOption env variables expected") 652 | } 653 | 654 | found := false 655 | for _,s := range mountCmd.Env { 656 | if s == "PASSWD=pass123" { 657 | found = true 658 | break 659 | } 660 | } 661 | if !found { 662 | t.Error("TestPassEnvOption env variable expected PASSWD=pass123") 663 | } 664 | 665 | if len(mountCmd.Args) != len(expected) { 666 | t.Errorf("TestPassEnvOption len: expected %d, actual %d", len(mountCmd.Args), len(expected)) 667 | } 668 | 669 | for idx := range expected { 670 | if mountCmd.Args[idx] != expected[idx] { 671 | t.Errorf("TestPassEnvOption[%d]: expected %s, actual %s", idx, expected[idx], mountCmd.Args[idx]) 672 | } 673 | } 674 | } 675 | 676 | func TestIncorrectJsonPayload(t *testing.T) { 677 | 678 | jsonArgs := fmt.Sprintf(`{ 679 | "kubernetes.io/mounterArgs.FsGroup": "33", 680 | "kubernetes.io/fsType": "", 681 | "kubernetes.io/pod.name": "nginx-deployment-549ddfb5fc-rnqk8", 682 | "kubernetes.io/pod.namespace": "default", 683 | "kubernetes.io/pod.uid": "bb6b2e46-c80d-4c86-920c-8e08736fa211", 684 | "kubernetes.io/pvOrVolumeName": "test-volume", 685 | "kubernetes.io/serviceAccount.name": "default", 686 | "kubernetes.io/secret/domain": "%s", 687 | "kubernetes.io/secret/username": "%s", 688 | "kubernetes.io/secret/password": "%s", 689 | "server": "fooserver123", 690 | "share": "/test", 691 | "passwdMethod": 999 692 | }`, 693 | base64.StdEncoding.EncodeToString([]byte("domain123\n\r")), 694 | base64.StdEncoding.EncodeToString([]byte("user123\n\r")), 695 | base64.StdEncoding.EncodeToString([]byte("pass123\n\r")), 696 | ) 697 | 698 | args := []string{"/path/to/binary", "mount", "/mnt/point", jsonArgs} 699 | msg := driverMain(args) 700 | 701 | expectedStatus := "Failure" 702 | expectedMessage := "Unexpected executing volume driver: Error interpreting mounter args" 703 | 704 | if msg.Status != expectedStatus { 705 | t.Errorf("TestIncorrectJsonPayload: expected status [%s] does not match [%s]", expectedStatus, msg.Status) 706 | } 707 | if !strings.Contains(msg.Message, expectedMessage) { 708 | t.Errorf("TestIncorrectJsonPayload: expected status [%s] does not match [%s]", expectedMessage, msg.Message) 709 | } 710 | } 711 | --------------------------------------------------------------------------------