Compare commits

...

10 Commits

Author SHA1 Message Date
Ruakij 9edb044d35 Use helm for install 4 months ago
Ruakij a3414c37dd Move build-system to mutli-stage build-container 4 months ago
Ruakij 2287670f63 Update package name and update package versions 4 months ago
sys-liqian 680a68204e docs: 📝 update README.md
update  README.md
8 months ago
sys-liqian e42800379a chore: update dockerfile
update dockerfile
8 months ago
sys-liqian ff95639565 fix: 🐛 fix umount webdav server
fix umount webdav server

Signed-off-by: sys-liqian <lq1083301982@163.com>
1 year ago
sys-liqian 7051bb12b2
Create README.md 1 year ago
sys-liqian 16e0a83b1a fix: 🐛 fix pod mount webdav server error
fix pod mount webdav server error
1 year ago
sys-liqian df80de8392 fix: 🐛 fix force umount
fix force umount
1 year ago
sys-liqian f8aa58a3be Initial commit
Signed-off-by: sys-liqian <lq1083301982@163.com>
1 year ago

72
.gitignore vendored

@ -0,0 +1,72 @@
# OSX leaves these everywhere on SMB shares
._*
# OSX trash
.DS_Store
# Eclipse files
.classpath
.project
.settings/**
# Files generated by JetBrains IDEs, e.g. IntelliJ IDEA
.idea/
*.iml
# Vscode files
.vscode
# This is where the result of the go build goes
/output*/
/_output*/
/_output
/bin
# Emacs save files
*~
\#*\#
.\#*
# Vim-related files
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
# cscope-related files
cscope.*
# Go test binaries
*.test
# JUnit test output from ginkgo e2e tests
/junit*.xml
# Mercurial files
**/.hg
**/.hg*
# Vagrant
.vagrant
.tags*
# Test artifacts produced by Jenkins jobs
/_artifacts/
# Go dependencies installed on Jenkins
/_gopath/
# direnv .envrc files
.envrc
# This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored
!\.drone\.sec
# Godeps or dep workspace
/Godeps/_workspace
/bazel-*
*.pyc
profile.cov

@ -0,0 +1,34 @@
# Copyright 2023.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ---- Build ----
FROM golang:1.23-alpine AS build
WORKDIR /build
# Copy sources
ADD . .
# Get dependencies
RUN make go-build
# Compile
RUN CGO_ENABLED=0 go build -a -o webdavplugin ./cmd/webdav
# ---- Release ----
FROM alpine AS release
# Install required packages
RUN apk add --no-cache davfs2
# Copy build-target
COPY --from=build /build/webdavplugin .
ENTRYPOINT ["/webdavplugin"]

@ -0,0 +1,41 @@
# Webdav CSI driver for Kubernetes
### Overview
This is a repository for webdav csi driver, csi plugin name: `webdav.csi.io`. This driver supports dynamic provisioning of Persistent Volumes via Persistent Volume Claims by creating a new sub directory under webdav server.
### Deploy CSI
#### With Helm
```bash
helm install -n webdav-csi-driver webdav-csi-driver helm/
```
### Quick start with kind
#### Build plugin image
```bash
make docker-build
```
#### Start kind cluster
```bash
kind create cluster --image kindest/node:v1.27.3
```
### Load plugin image to kind cluster
```bash
kind load docker-image registry.k8s.io/sig-storage/csi-provisioner:v3.6.2
kind load docker-image registry.k8s.io/sig-storage/livenessprobe:v2.11.0
kind load docker-image registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.1
kind load docker-image localhost:5000/webdavplugin:v0.0.1
```
### Tests
```bash
kubectl apply -f examples/csi-webdav-secret.yaml
kubectl apply -f examples/csi-webdav-storageclass.yaml
kubectl apply -f examples/csi-webdav-dynamic-pvc.yaml
kubectl apply -f examples/csi-webdav-pod.yaml
```

@ -0,0 +1,55 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"os"
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav"
"k8s.io/klog/v2"
)
var (
endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI endpoint")
nodeID = flag.String("nodeid", "", "node id")
mountPermissions = flag.Uint64("mount-permissions", 0, "mounted folder permissions")
driverName = flag.String("drivername", "", "name of the driver")
workingMountDir = flag.String("working-mount-dir", "/tmp/csi-storage", "working directory for provisioner to mount davfs shares temporarily")
defaultOnDeletePolicy = flag.String("default-ondelete-policy", "", "default policy for deleting subdirectory when deleting a volume")
)
func main() {
klog.InitFlags(nil)
_ = flag.Set("logtostderr", "true")
flag.Parse()
if *nodeID == "" {
klog.Warning("nodeid is empty")
}
driverOptions := webdav.DriverOpt{
Name: *driverName,
NodeID: *nodeID,
Endpoint: *endpoint,
MountPermissions: *mountPermissions,
WorkingMountDir: *workingMountDir,
DefaultOnDeletePolicy: *defaultOnDeletePolicy,
}
d := webdav.NewDriver(&driverOptions)
d.Run()
os.Exit(0)
}

@ -0,0 +1,12 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-webdav-dynamic
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: webdav-sc

@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: pvc-webdav-dynamic
mountPath: /var/www/html
volumes:
- name: pvc-webdav-dynamic
persistentVolumeClaim:
claimName: pvc-webdav-dynamic

@ -0,0 +1,9 @@
---
apiVersion: v1
kind: Secret
metadata:
name: webdav-secrect
type: Opaque
data:
username: YWRtaW4=
password: YWRtaW4=

@ -0,0 +1,16 @@
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: webdav-sc
provisioner: webdav.csi.io
parameters:
# alist folder webdav address
share: http://ip:port/dav/media
csi.storage.k8s.io/provisioner-secret-name: "webdav-secrect"
csi.storage.k8s.io/provisioner-secret-namespace: "default"
csi.storage.k8s.io/node-publish-secret-name: "webdav-secrect"
csi.storage.k8s.io/node-publish-secret-namespace: "default"
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:

@ -0,0 +1,24 @@
module git.ruekov.eu/ruakij/webdav-csi-driver
go 1.23
require (
github.com/container-storage-interface/spec v1.10.0
github.com/moby/sys/mountinfo v0.7.2
golang.org/x/sys v0.24.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20240821151609-f90d01438635
sigs.k8s.io/yaml v1.4.0
)
require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

@ -0,0 +1,42 @@
github.com/container-storage-interface/spec v1.10.0 h1:YkzWPV39x+ZMTa6Ax2czJLLwpryrQ+dPesB34mrRMXA=
github.com/container-storage-interface/spec v1.10.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI=
k8s.io/utils v0.0.0-20240821151609-f90d01438635/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

@ -0,0 +1,4 @@
apiVersion: v2
name: webdav-csi-driver
description: A Helm chart for deploying CSI WebDAV Storage Driver
version: 0.0.1

@ -0,0 +1,124 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webdav-csi-controller
namespace: kube-system
spec:
replicas: {{ .Values.controller.replicas }}
selector:
matchLabels:
app: webdav-csi-controller
template:
metadata:
labels:
app: webdav-csi-controller
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: webdav-csi-sa
nodeSelector:
kubernetes.io/os: linux
priorityClassName: system-cluster-critical
securityContext:
seccompProfile:
type: RuntimeDefault
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/controlplane"
operator: "Exists"
effect: "NoSchedule"
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: csi-provisioner
image: {{ .Values.csiProvisioner.image.name }}:{{ .Values.csiProvisioner.image.tag }}
imagePullPolicy: {{ .Values.csiProvisioner.image.pullPolicy }}
args:
- "-v=2"
- "--csi-address=$(ADDRESS)"
- "--leader-election"
- "--leader-election-namespace=kube-system"
- "--extra-create-metadata=true"
- "--timeout=1200s"
env:
- name: ADDRESS
value: /csi/csi.sock
volumeMounts:
- mountPath: /csi
name: socket-dir
resources:
limits:
memory: {{ .Values.controller.resources.limits.memory }}
requests:
cpu: {{ .Values.controller.resources.requests.cpu }}
memory: {{ .Values.controller.resources.requests.memory }}
- name: liveness-probe
image: {{ .Values.livenessProbe.image.name }}:{{ .Values.livenessProbe.image.tag }}
imagePullPolicy: {{ .Values.livenessProbe.image.pullPolicy }}
args:
- --csi-address=/csi/csi.sock
- --probe-timeout=3s
- --health-port=29652
- --v=2
volumeMounts:
- name: socket-dir
mountPath: /csi
resources:
limits:
memory: 100Mi
requests:
cpu: 10m
memory: 20Mi
- name: webdav
image: {{ .Values.controller.image.name }}:{{ .Values.controller.image.tag }}
imagePullPolicy: {{ .Values.controller.image.pullPolicy }}
securityContext:
privileged: true
capabilities:
add: ["SYS_ADMIN"]
allowPrivilegeEscalation: true
args:
- "-v=5"
- "--nodeid=$(NODE_ID)"
- "--endpoint=$(CSI_ENDPOINT)"
env:
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CSI_ENDPOINT
value: unix:///csi/csi.sock
ports:
- containerPort: 29652
name: healthz
protocol: TCP
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: healthz
initialDelaySeconds: 30
timeoutSeconds: 10
periodSeconds: 30
volumeMounts:
- name: pods-mount-dir
mountPath: /var/lib/kubelet/pods
mountPropagation: "Bidirectional"
- mountPath: /csi
name: socket-dir
resources:
limits:
memory: {{ .Values.controller.resources.limits.memory }}
requests:
cpu: {{ .Values.controller.resources.requests.cpu }}
memory: {{ .Values.controller.resources.requests.memory }}
volumes:
- name: pods-mount-dir
hostPath:
path: /var/lib/kubelet/pods
type: Directory
- name: socket-dir
emptyDir: {}

@ -0,0 +1,13 @@
{{- if .defaultStorageClass }}
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: {{ .defaultStorageClass.name }}
provisioner: webdav.csi.io
parameters:
{{- toYaml .defaultStorageClass.parameters | nindent 2 }}
reclaimPolicy: {{ .defaultStorageClass.reclaimPolicy }}
volumeBindingMode: {{ .defaultStorageClass.volumeBindingMode }}
mountOptions:
{{- toYaml .defaultStorageClass.mountOptions | nindent 2 }}
{{- end }}

@ -0,0 +1,8 @@
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
name: webdav.csi.io
spec:
attachRequired: false
volumeLifecycleModes:
- Persistent

@ -0,0 +1,136 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: webdav-csi-node
namespace: kube-system
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
selector:
matchLabels:
app: webdav-csi-node
template:
metadata:
labels:
app: webdav-csi-node
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: webdav-csi-sa
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
nodeSelector:
kubernetes.io/os: linux
tolerations:
- operator: "Exists"
containers:
- name: liveness-probe
image: {{ .Values.livenessProbe.image.name }}:{{ .Values.livenessProbe.image.tag }}
imagePullPolicy: {{ .Values.livenessProbe.image.pullPolicy }}
args:
- --csi-address=/csi/csi.sock
- --probe-timeout=3s
- --health-port=29653
- --v=2
volumeMounts:
- name: socket-dir
mountPath: /csi
resources:
limits:
memory: 100Mi
requests:
cpu: 10m
memory: 20Mi
- name: node-driver-registrar
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.1
imagePullPolicy: IfNotPresent
args:
- --v=2
- --csi-address=/csi/csi.sock
- --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
livenessProbe:
exec:
command:
- /csi-node-driver-registrar
- --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
- --mode=kubelet-registration-probe
initialDelaySeconds: 30
timeoutSeconds: 15
env:
- name: DRIVER_REG_SOCK_PATH
value: /var/lib/kubelet/plugins/webdav-csiplugin/csi.sock
- name: KUBE_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
volumeMounts:
- name: socket-dir
mountPath: /csi
- name: registration-dir
mountPath: /registration
resources:
limits:
memory: 100Mi
requests:
cpu: 10m
memory: 20Mi
- name: webdav
securityContext:
privileged: true
capabilities:
add: ["SYS_ADMIN"]
allowPrivilegeEscalation: true
image: {{ .Values.node.image.name }}:{{ .Values.node.image.tag }}
imagePullPolicy: {{ .Values.node.image.pullPolicy }}
args:
- "-v=5"
- "--nodeid=$(NODE_ID)"
- "--endpoint=$(CSI_ENDPOINT)"
env:
- name: NODE_ID
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: CSI_ENDPOINT
value: unix:///csi/csi.sock
ports:
- containerPort: 29653
name: healthz
protocol: TCP
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: healthz
initialDelaySeconds: 30
timeoutSeconds: 10
periodSeconds: 30
volumeMounts:
- name: socket-dir
mountPath: /csi
- name: pods-mount-dir
mountPath: /var/lib/kubelet/pods
mountPropagation: "Bidirectional"
resources:
limits:
memory: {{ .Values.node.resources.limits.memory }}
requests:
cpu: {{ .Values.node.resources.requests.cpu }}
memory: {{ .Values.node.resources.requests.memory }}
volumes:
- name: socket-dir
hostPath:
path: /var/lib/kubelet/plugins/webdav-csiplugin
type: DirectoryOrCreate
- name: pods-mount-dir
hostPath:
path: /var/lib/kubelet/pods
type: Directory
- hostPath:
path: /var/lib/kubelet/plugins_registry
type: Directory
name: registration-dir

@ -0,0 +1,48 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: webdav-csi-sa
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: webdav-csi-cr
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["list", "watch", "create", "update", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["csinodes"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "watch", "list", "delete", "update", "create"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: webdav-csi-crb
subjects:
- kind: ServiceAccount
name: webdav-csi-sa
namespace: kube-system
roleRef:
kind: ClusterRole
name: webdav-csi-cr
apiGroup: rbac.authorization.k8s.io

@ -0,0 +1,50 @@
controller:
replicas: 1
image:
name: ghcr.io/ruakij/webdav-csi-driver
tag: v0.0.1
pullPolicy: IfNotPresent
resources:
limits:
memory: 200Mi
requests:
cpu: 10m
memory: 20Mi
node:
image:
name: ghcr.io/ruakij/webdav-csi-driver
tag: v0.0.1
pullPolicy: IfNotPresent
resources:
limits:
memory: 300Mi
requests:
cpu: 10m
memory: 20Mi
livenessProbe:
image:
name: registry.k8s.io/sig-storage/livenessprobe
tag: v2.11.0
pullPolicy: IfNotPresent
csiProvisioner:
image:
name: registry.k8s.io/sig-storage/csi-provisioner
tag: v3.6.2
pullPolicy: IfNotPresent
# Configuration for the default storage class
defaultStorageClass: {}
# name: "webdav"
# parameters:
# # alist folder webdav address
# share: http://ip:port/dav/media
# csi.storage.k8s.io/provisioner-secret-name: "webdav-secrect"
# csi.storage.k8s.io/provisioner-secret-namespace: "default"
# csi.storage.k8s.io/node-publish-secret-name: "webdav-secrect"
# csi.storage.k8s.io/node-publish-secret-namespace: "default"
# reclaimPolicy: "Delete"
# volumeBindingMode: Immediate
# mountOptions: {}

@ -0,0 +1,248 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/klog/v2"
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
)
type ControllerServer struct {
csi.UnimplementedControllerServer
*Driver
mounter mount.Interface
}
func NewControllerServer(d *Driver, mounter mount.Interface) *ControllerServer {
return &ControllerServer{
Driver: d,
mounter: mounter,
}
}
// CreateVolume implements csi.ControllerServer.
func (c *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
name := req.GetName()
if len(name) == 0 {
return nil, status.Error(codes.InvalidArgument, "CreateVolume name must be provided")
}
if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
mountPermissions := c.Driver.mountPermissions
parameters := req.GetParameters()
if parameters == nil {
parameters = make(map[string]string)
}
for k, v := range parameters {
switch strings.ToLower(k) {
case webdavSharePath, pvcNameKey, pvcNamespaceKey, pvNameKey:
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))
}
}
targetPath := c.workingMountDir
sourcePath := req.Parameters[webdavSharePath]
notMnt, err := c.mounter.IsLikelyNotMountPoint(targetPath)
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(targetPath, 0750); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
notMnt = true
} else {
return nil, status.Error(codes.Internal, err.Error())
}
}
if !notMnt {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("target path %s is alredy mounted", targetPath))
}
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error()))
}
internalVolumePath := filepath.Join(targetPath, req.Name)
if err = os.Mkdir(internalVolumePath, 0777); err != nil && !os.IsExist(err) {
return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error())
}
defer func() {
if err = c.mounter.Unmount(targetPath); err != nil {
klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error())
}
}()
if mountPermissions > 0 {
// Reset directory permissions because of umask problems
if err = os.Chmod(internalVolumePath, os.FileMode(mountPermissions)); err != nil {
klog.Warningf("failed to chmod subdirectory: %v", err.Error())
}
}
return &csi.CreateVolumeResponse{
Volume: &csi.Volume{
VolumeId: MakeVolumeId(sourcePath, req.Name),
CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
VolumeContext: nil,
ContentSource: req.GetVolumeContentSource(),
},
}, nil
}
// DeleteVolume implements csi.ControllerServer.
func (c *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
volumeID := req.GetVolumeId()
if volumeID == "" {
return nil, status.Error(codes.InvalidArgument, "volume id is empty")
}
sourcePath, subDir, err := ParseVolumeId(volumeID)
if err != nil {
// An invalid ID should be treated as doesn't exist
klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err)
return &csi.DeleteVolumeResponse{}, nil
}
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
targetPath := c.workingMountDir
if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil {
return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error()))
}
defer func() {
if err = c.mounter.Unmount(targetPath); err != nil {
klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error())
}
}()
internalVolumePath := filepath.Join(targetPath, subDir)
klog.V(2).Infof("Removing subdirectory at %v", internalVolumePath)
if err = os.RemoveAll(internalVolumePath); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete subdirectory: %v", err.Error())
}
return &csi.DeleteVolumeResponse{}, nil
}
// ValidateVolumeCapabilities implements csi.ControllerServer.
func (c *ControllerServer) ValidateVolumeCapabilities(_ context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) {
if len(req.GetVolumeId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
}
if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &csi.ValidateVolumeCapabilitiesResponse{
Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{
VolumeCapabilities: req.GetVolumeCapabilities(),
},
Message: "",
}, nil
}
// ControllerGetCapabilities implements csi.ControllerServer.
func (c *ControllerServer) ControllerGetCapabilities(context.Context, *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
return &csi.ControllerGetCapabilitiesResponse{
Capabilities: c.Driver.cscap,
}, nil
}
// ControllerExpandVolume implements csi.ControllerServer.
func (*ControllerServer) ControllerExpandVolume(context.Context, *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ControllerGetVolume implements csi.ControllerServer.
func (*ControllerServer) ControllerGetVolume(context.Context, *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ControllerModifyVolume implements csi.ControllerServer.
func (*ControllerServer) ControllerModifyVolume(context.Context, *csi.ControllerModifyVolumeRequest) (*csi.ControllerModifyVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ControllerPublishVolume implements csi.ControllerServer.
func (*ControllerServer) ControllerPublishVolume(context.Context, *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ControllerUnpublishVolume implements csi.ControllerServer.
func (*ControllerServer) ControllerUnpublishVolume(context.Context, *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// CreateSnapshot implements csi.ControllerServer.
func (*ControllerServer) CreateSnapshot(context.Context, *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// DeleteSnapshot implements csi.ControllerServer.
func (*ControllerServer) DeleteSnapshot(context.Context, *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// GetCapacity implements csi.ControllerServer.
func (*ControllerServer) GetCapacity(context.Context, *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ListSnapshots implements csi.ControllerServer.
func (*ControllerServer) ListSnapshots(context.Context, *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// ListVolumes implements csi.ControllerServer.
func (*ControllerServer) ListVolumes(context.Context, *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// isValidVolumeCapabilities validates the given VolumeCapability array is valid
func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
if len(volCaps) == 0 {
return fmt.Errorf("volume capabilities missing in request")
}
for _, c := range volCaps {
if c.GetBlock() != nil {
return fmt.Errorf("block volume capability not supported")
}
}
return nil
}

@ -0,0 +1,122 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
"github.com/container-storage-interface/spec/lib/go/csi"
"k8s.io/klog/v2"
)
const (
DefaultDriverName = "webdav.csi.io"
fstype = "davfs"
webdavSharePath = "share"
mountPermissionsField = "mountpermissions"
pvcNameKey = "csi.storage.k8s.io/pvc/name"
pvcNamespaceKey = "csi.storage.k8s.io/pvc/namespace"
pvNameKey = "csi.storage.k8s.io/pv/name"
secretUsernameKey = "username"
secretPasswordKey = "password"
)
type Driver struct {
name string
nodeID string
endpoint string
version string
mountPermissions uint64
workingMountDir string
defaultOnDeletePolicy string
cscap []*csi.ControllerServiceCapability
nscap []*csi.NodeServiceCapability
}
type DriverOpt struct {
Name string
NodeID string
Endpoint string
MountPermissions uint64
WorkingMountDir string
DefaultOnDeletePolicy string
}
func NewDriver(opt *DriverOpt) *Driver {
klog.V(2).Infof("Driver: %v version: %v", opt.Name, driverVersion)
driverName := opt.Name
if driverName == "" {
driverName = DefaultDriverName
}
driver := &Driver{
name: driverName,
nodeID: opt.NodeID,
endpoint: opt.Endpoint,
mountPermissions: opt.MountPermissions,
workingMountDir: opt.WorkingMountDir,
defaultOnDeletePolicy: opt.DefaultOnDeletePolicy,
version: driverName,
}
driver.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
})
driver.AddNodeServiceCapabilities([]csi.NodeServiceCapability_RPC_Type{
csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
csi.NodeServiceCapability_RPC_UNKNOWN,
})
return driver
}
func (d *Driver) Run() {
versionMeta, err := GetVersionYAML(d.name)
if err != nil {
klog.Fatalf("%v", err)
}
klog.V(2).Infof("\nDRIVER INFORMATION:\n-------------------\n%s\n\nStreaming logs below:", versionMeta)
mounter := mount.New("")
server := NewNonBlockingGRPCServer()
server.Start(d.endpoint,
NewIdentityServer(d),
NewControllerServer(d, mounter),
NewNodeServer(d, mounter),
)
server.Wait()
}
func (d *Driver) AddControllerServiceCapabilities(cl []csi.ControllerServiceCapability_RPC_Type) {
var csc []*csi.ControllerServiceCapability
for _, c := range cl {
csc = append(csc, NewControllerServiceCapability(c))
}
d.cscap = csc
}
func (d *Driver) AddNodeServiceCapabilities(nl []csi.NodeServiceCapability_RPC_Type) {
var nsc []*csi.NodeServiceCapability
for _, n := range nl {
nsc = append(nsc, NewNodeServiceCapability(n))
}
d.nscap = nsc
}

@ -0,0 +1,70 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"context"
"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
)
type IdentityServer struct {
csi.UnimplementedIdentityServer
Driver *Driver
}
func NewIdentityServer(d *Driver) *IdentityServer {
return &IdentityServer{
Driver: d,
}
}
func (ids *IdentityServer) GetPluginInfo(_ context.Context, _ *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
if ids.Driver.name == "" {
return nil, status.Error(codes.Unavailable, "Driver name not configured")
}
if ids.Driver.version == "" {
return nil, status.Error(codes.Unavailable, "Driver is missing version")
}
return &csi.GetPluginInfoResponse{
Name: ids.Driver.name,
VendorVersion: ids.Driver.version,
}, nil
}
func (ids *IdentityServer) Probe(_ context.Context, _ *csi.ProbeRequest) (*csi.ProbeResponse, error) {
return &csi.ProbeResponse{Ready: &wrapperspb.BoolValue{Value: true}}, nil
}
func (ids *IdentityServer) GetPluginCapabilities(_ context.Context, _ *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
return &csi.GetPluginCapabilitiesResponse{
Capabilities: []*csi.PluginCapability{
{
Type: &csi.PluginCapability_Service_{
Service: &csi.PluginCapability_Service{
Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
},
},
},
},
}, nil
}

@ -0,0 +1,405 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have
// an alternate platform, we will need to abstract further.
package mount
import (
"fmt"
"path/filepath"
"strings"
"time"
utilexec "k8s.io/utils/exec"
)
const (
// Default mount command if mounter path is not specified.
defaultMountCommand = "mount"
// Log message where sensitive mount options were removed
sensitiveOptionsRemoved = "<masked>"
)
// Interface defines the set of methods to allow for mount operations on a system.
type Interface interface {
// Mount mounts source to target as fstype with given options.
// options MUST not contain sensitive material (like passwords).
Mount(source string, target string, fstype string, options []string) error
// MountSensitive is the same as Mount() but this method allows
// sensitiveOptions to be passed in a separate parameter from the normal
// mount options and ensures the sensitiveOptions are never logged. This
// method should be used by callers that pass sensitive material (like
// passwords) as mount options.
MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error
// MountSensitiveWithStdin
MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error
// MountSensitiveWithoutSystemd is the same as MountSensitive() but this method disable using systemd mount.
MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error
// MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd() with additional mount flags
MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error
// Unmount unmounts given target.
Unmount(target string) error
// List returns a list of all mounted filesystems. This can be large.
// On some platforms, reading mounts directly from the OS is not guaranteed
// consistent (i.e. it could change between chunked reads). This is guaranteed
// to be consistent.
List() ([]MountPoint, error)
// IsLikelyNotMountPoint uses heuristics to determine if a directory
// is not a mountpoint.
// It should return ErrNotExist when the directory does not exist.
// IsLikelyNotMountPoint does NOT properly detect all mountpoint types
// most notably linux bind mounts and symbolic link. For callers that do not
// care about such situations, this is a faster alternative to calling List()
// and scanning that output.
IsLikelyNotMountPoint(file string) (bool, error)
// CanSafelySkipMountPointCheck indicates whether this mounter returns errors on
// operations for targets that are not mount points. If this returns true, no such
// errors will be returned.
CanSafelySkipMountPointCheck() bool
// IsMountPoint determines if a directory is a mountpoint.
// It should return ErrNotExist when the directory does not exist.
// IsMountPoint is more expensive than IsLikelyNotMountPoint.
// IsMountPoint detects bind mounts in linux.
// IsMountPoint may enumerate all the mountpoints using List() and
// the list of mountpoints may be large, then it uses
// isMountPointMatch to evaluate whether the directory is a mountpoint.
IsMountPoint(file string) (bool, error)
// GetMountRefs finds all mount references to pathname, returning a slice of
// paths. Pathname can be a mountpoint path or a normal directory
// (for bind mount). On Linux, pathname is excluded from the slice.
// For example, if /dev/sdc was mounted at /path/a and /path/b,
// GetMountRefs("/path/a") would return ["/path/b"]
// GetMountRefs("/path/b") would return ["/path/a"]
// On Windows there is no way to query all mount points; as long as pathname is
// a valid mount, it will be returned.
GetMountRefs(pathname string) ([]string, error)
}
// Compile-time check to ensure all Mounter implementations satisfy
// the mount interface.
var _ Interface = &Mounter{}
type MounterForceUnmounter interface {
Interface
// UnmountWithForce unmounts given target but will retry unmounting with force option
// after given timeout.
UnmountWithForce(target string, umountTimeout time.Duration) error
}
// MountPoint represents a single line in /proc/mounts or /etc/fstab.
type MountPoint struct { // nolint: golint
Device string
Path string
Type string
Opts []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged).
Freq int
Pass int
}
type MountErrorType string // nolint: golint
const (
FilesystemMismatch MountErrorType = "FilesystemMismatch"
HasFilesystemErrors MountErrorType = "HasFilesystemErrors"
UnformattedReadOnly MountErrorType = "UnformattedReadOnly"
FormatFailed MountErrorType = "FormatFailed"
GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed"
UnknownMountError MountErrorType = "UnknownMountError"
)
type MountError struct { // nolint: golint
Type MountErrorType
Message string
}
func (mountError MountError) String() string {
return mountError.Message
}
func (mountError MountError) Error() string {
return mountError.Message
}
func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error {
mountError := MountError{
Type: mountErrorValue,
Message: fmt.Sprintf(format, args...),
}
return mountError
}
// SafeFormatAndMount probes a device to see if it is formatted.
// Namely it checks to see if a file system is present. If so it
// mounts it otherwise the device is formatted first then mounted.
type SafeFormatAndMount struct {
Interface
Exec utilexec.Interface
formatSem chan any
formatTimeout time.Duration
}
func NewSafeFormatAndMount(mounter Interface, exec utilexec.Interface, opts ...Option) *SafeFormatAndMount {
res := &SafeFormatAndMount{
Interface: mounter,
Exec: exec,
}
for _, opt := range opts {
opt(res)
}
return res
}
type Option func(*SafeFormatAndMount)
// WithMaxConcurrentFormat sets the maximum number of concurrent format
// operations executed by the mounter. The timeout controls the maximum
// duration of a format operation before its concurrency token is released.
// Once a token is released, it can be acquired by another concurrent format
// operation. The original operation is allowed to complete.
// If n < 1, concurrency is set to unlimited.
func WithMaxConcurrentFormat(n int, timeout time.Duration) Option {
return func(mounter *SafeFormatAndMount) {
if n > 0 {
mounter.formatSem = make(chan any, n)
mounter.formatTimeout = timeout
}
}
}
// FormatAndMount formats the given disk, if needed, and mounts it.
// That is if the disk is not formatted and it is not being mounted as
// read-only it will format it first then mount it. Otherwise, if the
// disk is already formatted or it is being mounted as read-only, it
// will be mounted without formatting.
// options MUST not contain sensitive material (like passwords).
func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error {
return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
}
// FormatAndMountSensitive is the same as FormatAndMount but this method allows
// sensitiveOptions to be passed in a separate parameter from the normal mount
// options and ensures the sensitiveOptions are never logged. This method should
// be used by callers that pass sensitive material (like passwords) as mount
// options.
func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
return mounter.FormatAndMountSensitiveWithFormatOptions(source, target, fstype, options, sensitiveOptions, nil /* formatOptions */)
}
// FormatAndMountSensitiveWithFormatOptions behaves exactly the same as
// FormatAndMountSensitive, but allows for options to be passed when the disk
// is formatted. These options are NOT validated in any way and should never
// come directly from untrusted user input as that would be an injection risk.
func (mounter *SafeFormatAndMount) FormatAndMountSensitiveWithFormatOptions(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error {
return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions, formatOptions)
}
// getMountRefsByDev finds all references to the device provided
// by mountPath; returns a list of paths.
// Note that mountPath should be path after the evaluation of any symblolic links.
//
//lint:ignore U1000 Ignore unused function temporarily for debugging
func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) {
mps, err := mounter.List()
if err != nil {
return nil, err
}
// Finding the device mounted to mountPath.
diskDev := ""
for i := range mps {
if mountPath == mps[i].Path {
diskDev = mps[i].Device
break
}
}
// Find all references to the device.
var refs []string
for i := range mps {
if mps[i].Device == diskDev || mps[i].Device == mountPath {
if mps[i].Path != mountPath {
refs = append(refs, mps[i].Path)
}
}
}
return refs, nil
}
// IsNotMountPoint determines if a directory is a mountpoint.
// It should return ErrNotExist when the directory does not exist.
// IsNotMountPoint is more expensive than IsLikelyNotMountPoint
// and depends on IsMountPoint.
//
// If an error occurs, it returns true (assuming it is not a mountpoint)
// when ErrNotExist is returned for callers similar to IsLikelyNotMountPoint.
//
// Deprecated: This function is kept to keep changes backward compatible with
// previous library version. Callers should prefer mounter.IsMountPoint.
func IsNotMountPoint(mounter Interface, file string) (bool, error) {
isMnt, err := mounter.IsMountPoint(file)
if err != nil {
return true, err
}
return !isMnt, nil
}
// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts
// returns the device name, reference count, and error code.
func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) {
mps, err := mounter.List()
if err != nil {
return "", 0, err
}
// Find the device name.
// FIXME if multiple devices mounted on the same mount path, only the first one is returned.
device := ""
// If mountPath is symlink, need get its target path.
slTarget, err := filepath.EvalSymlinks(mountPath)
if err != nil {
slTarget = mountPath
}
for i := range mps {
if mps[i].Path == slTarget {
device = mps[i].Device
break
}
}
// Find all references to the device.
refCount := 0
for i := range mps {
if mps[i].Device == device {
refCount++
}
}
return device, refCount, nil
}
// MakeBindOpts detects whether a bind mount is being requested and makes the remount options to
// use in case of bind mount, due to the fact that bind mount doesn't respect mount options.
// The list equals:
//
// options - 'bind' + 'remount' (no duplicate)
func MakeBindOpts(options []string) (bool, []string, []string) {
bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */)
return bind, bindOpts, bindRemountOpts
}
// MakeBindOptsSensitive is the same as MakeBindOpts but this method allows
// sensitiveOptions to be passed in a separate parameter from the normal mount
// options and ensures the sensitiveOptions are never logged. This method should
// be used by callers that pass sensitive material (like passwords) as mount
// options.
func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) {
// Because we have an FD opened on the subpath bind mount, the "bind" option
// needs to be included, otherwise the mount target will error as busy if you
// remount as readonly.
//
// As a consequence, all read only bind mounts will no longer change the underlying
// volume mount to be read only.
bindRemountOpts := []string{"bind", "remount"}
bindRemountSensitiveOpts := []string{}
bind := false
bindOpts := []string{"bind"}
// _netdev is a userspace mount option and does not automatically get added when
// bind mount is created and hence we must carry it over.
if checkForNetDev(options, sensitiveOptions) {
bindOpts = append(bindOpts, "_netdev")
}
for _, option := range options {
switch option {
case "bind":
bind = true
case "remount":
default:
bindRemountOpts = append(bindRemountOpts, option)
}
}
for _, sensitiveOption := range sensitiveOptions {
switch sensitiveOption {
case "bind":
bind = true
case "remount":
default:
bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption)
}
}
return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts
}
func checkForNetDev(options []string, sensitiveOptions []string) bool {
for _, option := range options {
if option == "_netdev" {
return true
}
}
for _, sensitiveOption := range sensitiveOptions {
if sensitiveOption == "_netdev" {
return true
}
}
return false
}
// PathWithinBase checks if give path is within given base directory.
func PathWithinBase(fullPath, basePath string) bool {
rel, err := filepath.Rel(basePath, fullPath)
if err != nil {
return false
}
if StartsWithBackstep(rel) {
// Needed to escape the base path.
return false
}
return true
}
// StartsWithBackstep checks if the given path starts with a backstep segment.
func StartsWithBackstep(rel string) bool {
// normalize to / and check for ../
return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../")
}
// sanitizedOptionsForLogging will return a comma separated string containing
// options and sensitiveOptions. Each entry in sensitiveOptions will be
// replaced with the string sensitiveOptionsRemoved
// e.g. o1,o2,<masked>,<masked>
func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string {
separator := ""
if len(options) > 0 && len(sensitiveOptions) > 0 {
separator = ","
}
sensitiveOptionsStart := ""
sensitiveOptionsEnd := ""
if len(sensitiveOptions) > 0 {
sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1)
sensitiveOptionsEnd = sensitiveOptionsRemoved
}
return strings.Join(options, ",") +
separator +
sensitiveOptionsStart +
sensitiveOptionsEnd
}

@ -0,0 +1,157 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mount
import (
"fmt"
"os"
"time"
"k8s.io/klog/v2"
)
// CleanupMountPoint unmounts the given path and deletes the remaining directory
// if successful. If extensiveMountPointCheck is true IsNotMountPoint will be
// called instead of IsLikelyNotMountPoint. IsNotMountPoint is more expensive
// but properly handles bind mounts within the same fs.
func CleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) error {
pathExists, pathErr := PathExists(mountPath)
if !pathExists && pathErr == nil {
klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath)
return nil
}
corruptedMnt := IsCorruptedMnt(pathErr)
if pathErr != nil && !corruptedMnt {
return fmt.Errorf("Error checking path: %v", pathErr)
}
return doCleanupMountPoint(mountPath, mounter, extensiveMountPointCheck, corruptedMnt)
}
func CleanupMountWithForce(mountPath string, mounter MounterForceUnmounter, extensiveMountPointCheck bool, umountTimeout time.Duration) error {
pathExists, pathErr := PathExists(mountPath)
if !pathExists && pathErr == nil {
klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath)
return nil
}
corruptedMnt := IsCorruptedMnt(pathErr)
if pathErr != nil && !corruptedMnt {
return fmt.Errorf("Error checking path: %v", pathErr)
}
if corruptedMnt || mounter.CanSafelySkipMountPointCheck() {
klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)",
mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck())
if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil {
return err
}
return removePath(mountPath)
}
notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
// if mountPath is not a mount point, it's just been removed or there was an error
if err != nil || notMnt {
return err
}
klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath)
if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil {
return err
}
notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
// if mountPath is not a mount point, it's either just been removed or there was an error
if notMnt {
return err
}
// mountPath is still a mount point
return fmt.Errorf("failed to cleanup mount point %v", mountPath)
}
// doCleanupMountPoint unmounts the given path and
// deletes the remaining directory if successful.
// if extensiveMountPointCheck is true
// IsNotMountPoint will be called instead of IsLikelyNotMountPoint.
// IsNotMountPoint is more expensive but properly handles bind mounts within the same fs.
// if corruptedMnt is true, it means that the mountPath is a corrupted mountpoint, and the mount point check
// will be skipped. The mount point check will also be skipped if the mounter supports it.
func doCleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool, corruptedMnt bool) error {
if corruptedMnt || mounter.CanSafelySkipMountPointCheck() {
klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)",
mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck())
if err := mounter.Unmount(mountPath); err != nil {
return err
}
return removePath(mountPath)
}
notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
// if mountPath is not a mount point, it's just been removed or there was an error
if err != nil || notMnt {
return err
}
klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath)
if err := mounter.Unmount(mountPath); err != nil {
return err
}
notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
// if mountPath is not a mount point, it's either just been removed or there was an error
if notMnt {
return err
}
// mountPath is still a mount point
return fmt.Errorf("failed to cleanup mount point %v", mountPath)
}
// removePathIfNotMountPoint verifies if given mountPath is a mount point if not it attempts
// to remove the directory. Returns true and nil if directory was not a mount point and removed.
func removePathIfNotMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) (bool, error) {
var notMnt bool
var err error
if extensiveMountPointCheck {
notMnt, err = IsNotMountPoint(mounter, mountPath)
} else {
notMnt, err = mounter.IsLikelyNotMountPoint(mountPath)
}
if err != nil {
if os.IsNotExist(err) {
klog.V(4).Infof("%q does not exist", mountPath)
return true, nil
}
return notMnt, err
}
if notMnt {
klog.Warningf("Warning: %q is not a mountpoint, deleting", mountPath)
return notMnt, os.Remove(mountPath)
}
return notMnt, nil
}
// removePath attempts to remove the directory. Returns nil if the directory was removed or does not exist.
func removePath(mountPath string) error {
klog.V(4).Infof("Warning: deleting path %q", mountPath)
err := os.Remove(mountPath)
if os.IsNotExist(err) {
klog.V(4).Infof("%q does not exist", mountPath)
return nil
}
return err
}

@ -0,0 +1,250 @@
//go:build !windows
// +build !windows
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mount
import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"strconv"
"strings"
"sync"
"syscall"
"golang.org/x/sys/unix"
"k8s.io/klog/v2"
utilio "k8s.io/utils/io"
)
const (
// At least number of fields per line in /proc/<pid>/mountinfo.
expectedAtLeastNumFieldsPerMountInfo = 10
// How many times to retry for a consistent read of /proc/mounts.
maxListTries = 10
)
// IsCorruptedMnt return true if err is about corrupted mount point
func IsCorruptedMnt(err error) bool {
if err == nil {
return false
}
var underlyingError error
switch pe := err.(type) {
case nil:
return false
case *os.PathError:
underlyingError = pe.Err
case *os.LinkError:
underlyingError = pe.Err
case *os.SyscallError:
underlyingError = pe.Err
case syscall.Errno:
underlyingError = err
}
return underlyingError == syscall.ENOTCONN || underlyingError == syscall.ESTALE || underlyingError == syscall.EIO || underlyingError == syscall.EACCES || underlyingError == syscall.EHOSTDOWN
}
// MountInfo represents a single line in /proc/<pid>/mountinfo.
type MountInfo struct { // nolint: golint
// Unique ID for the mount (maybe reused after umount).
ID int
// The ID of the parent mount (or of self for the root of this mount namespace's mount tree).
ParentID int
// Major indicates one half of the device ID which identifies the device class
// (parsed from `st_dev` for files on this filesystem).
Major int
// Minor indicates one half of the device ID which identifies a specific
// instance of device (parsed from `st_dev` for files on this filesystem).
Minor int
// The pathname of the directory in the filesystem which forms the root of this mount.
Root string
// Mount source, filesystem-specific information. e.g. device, tmpfs name.
Source string
// Mount point, the pathname of the mount point.
MountPoint string
// Optional fieds, zero or more fields of the form "tag[:value]".
OptionalFields []string
// The filesystem type in the form "type[.subtype]".
FsType string
// Per-mount options.
MountOptions []string
// Per-superblock options.
SuperOptions []string
}
// ParseMountInfo parses /proc/xxx/mountinfo.
func ParseMountInfo(filename string) ([]MountInfo, error) {
content, err := readMountInfo(filename)
if err != nil {
return []MountInfo{}, err
}
contentStr := string(content)
infos := []MountInfo{}
for _, line := range strings.Split(contentStr, "\n") {
if line == "" {
// the last split() item is empty string following the last \n
continue
}
// See `man proc` for authoritative description of format of the file.
fields := strings.Fields(line)
if len(fields) < expectedAtLeastNumFieldsPerMountInfo {
return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line)
}
id, err := strconv.Atoi(fields[0])
if err != nil {
return nil, err
}
parentID, err := strconv.Atoi(fields[1])
if err != nil {
return nil, err
}
mm := strings.Split(fields[2], ":")
if len(mm) != 2 {
return nil, fmt.Errorf("parsing '%s' failed: unexpected minor:major pair %s", line, mm)
}
major, err := strconv.Atoi(mm[0])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: unable to parse major device id, err:%v", mm[0], err)
}
minor, err := strconv.Atoi(mm[1])
if err != nil {
return nil, fmt.Errorf("parsing '%s' failed: unable to parse minor device id, err:%v", mm[1], err)
}
info := MountInfo{
ID: id,
ParentID: parentID,
Major: major,
Minor: minor,
Root: fields[3],
MountPoint: fields[4],
MountOptions: splitMountOptions(fields[5]),
}
// All fields until "-" are "optional fields".
i := 6
for ; i < len(fields) && fields[i] != "-"; i++ {
info.OptionalFields = append(info.OptionalFields, fields[i])
}
// Parse the rest 3 fields.
i++
if len(fields)-i < 3 {
return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i)
}
info.FsType = fields[i]
info.Source = fields[i+1]
info.SuperOptions = splitMountOptions(fields[i+2])
infos = append(infos, info)
}
return infos, nil
}
// splitMountOptions parses comma-separated list of mount options into an array.
// It respects double quotes - commas in them are not considered as the option separator.
func splitMountOptions(s string) []string {
inQuotes := false
list := strings.FieldsFunc(s, func(r rune) bool {
if r == '"' {
inQuotes = !inQuotes
}
// Report a new field only when outside of double quotes.
return r == ',' && !inQuotes
})
return list
}
// isMountPointMatch returns true if the path in mp is the same as dir.
// Handles case where mountpoint dir has been renamed due to stale NFS mount.
func isMountPointMatch(mp MountPoint, dir string) bool {
return strings.TrimSuffix(mp.Path, "\\040(deleted)") == dir
}
// PathExists returns true if the specified path exists.
// TODO: clean this up to use pkg/util/file/FileExists
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
} else if errors.Is(err, fs.ErrNotExist) {
err = syscall.Access(path, syscall.F_OK)
if err == nil {
// The access syscall says the file exists, the stat syscall says it
// doesn't. This was observed on CIFS when the path was removed at
// the server somehow. POSIX calls this a stale file handle, let's fake
// that error and treat the path as existing but corrupted.
klog.Warningf("Potential stale file handle detected: %s", path)
return true, syscall.ESTALE
}
return false, nil
} else if IsCorruptedMnt(err) {
return true, err
}
return false, err
}
// These variables are used solely by kernelHasMountinfoBug.
var (
hasMountinfoBug bool
checkMountinfoBugOnce sync.Once
)
// kernelHasMountinfoBug checks if the kernel bug that can lead to incomplete
// mountinfo being read is fixed. It does so by checking the kernel version.
//
// The bug was fixed by the kernel commit 9f6c61f96f2d97 (since Linux 5.8).
// Alas, there is no better way to check if the bug is fixed other than to
// rely on the kernel version returned by uname.
func kernelHasMountinfoBug() bool {
checkMountinfoBugOnce.Do(func() {
// Assume old kernel.
hasMountinfoBug = true
uname := unix.Utsname{}
err := unix.Uname(&uname)
if err != nil {
return
}
end := bytes.IndexByte(uname.Release[:], 0)
v := bytes.SplitN(uname.Release[:end], []byte{'.'}, 3)
if len(v) != 3 {
return
}
major, _ := strconv.Atoi(string(v[0]))
minor, _ := strconv.Atoi(string(v[1]))
if major > 5 || (major == 5 && minor >= 8) {
hasMountinfoBug = false
}
})
return hasMountinfoBug
}
func readMountInfo(path string) ([]byte, error) {
if kernelHasMountinfoBug() {
return utilio.ConsistentRead(path, maxListTries)
}
return os.ReadFile(path)
}

@ -0,0 +1,871 @@
//go:build linux
// +build linux
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mount
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/moby/sys/mountinfo"
"k8s.io/klog/v2"
utilexec "k8s.io/utils/exec"
)
const (
// Number of fields per line in /proc/mounts as per the fstab man page.
expectedNumFieldsPerLine = 6
// Location of the mount file to use
procMountsPath = "/proc/mounts"
// Location of the mountinfo file
procMountInfoPath = "/proc/self/mountinfo"
// 'fsck' found errors and corrected them
fsckErrorsCorrected = 1
// 'fsck' found errors but exited without correcting them
fsckErrorsUncorrected = 4
// Error thrown by exec cmd.Run() when process spawned by cmd.Start() completes before cmd.Wait() is called (see - k/k issue #103753)
errNoChildProcesses = "wait: no child processes"
// Error returned by some `umount` implementations when the specified path is not a mount point
errNotMounted = "not mounted"
)
// Mounter provides the default implementation of mount.Interface
// for the linux platform. This implementation assumes that the
// kubelet is running in the host's root mount namespace.
type Mounter struct {
mounterPath string
withSystemd *bool
trySystemd bool
withSafeNotMountedBehavior bool
}
var _ MounterForceUnmounter = &Mounter{}
// New returns a mount.Interface for the current system.
// It provides options to override the default mounter behavior.
// mounterPath allows using an alternative to `/bin/mount` for mounting.
func New(mounterPath string) Interface {
return &Mounter{
mounterPath: mounterPath,
trySystemd: true,
withSafeNotMountedBehavior: detectSafeNotMountedBehavior(),
}
}
// NewWithoutSystemd returns a Linux specific mount.Interface for the current
// system. It provides options to override the default mounter behavior.
// mounterPath allows using an alternative to `/bin/mount` for mounting. Any
// detection for systemd functionality is disabled with this Mounter.
func NewWithoutSystemd(mounterPath string) Interface {
return &Mounter{
mounterPath: mounterPath,
trySystemd: false,
withSafeNotMountedBehavior: detectSafeNotMountedBehavior(),
}
}
// hasSystemd validates that the withSystemd bool is set, if it is not,
// detectSystemd will be called once for this Mounter instance.
func (mounter *Mounter) hasSystemd() bool {
if !mounter.trySystemd {
mounter.withSystemd = &mounter.trySystemd
}
if mounter.withSystemd == nil {
withSystemd := detectSystemd()
mounter.withSystemd = &withSystemd
}
return *mounter.withSystemd
}
// Mount mounts source to target as fstype with given options. 'source' and 'fstype' must
// be an empty string in case it's not required, e.g. for remount, or for auto filesystem
// type, where kernel handles fstype for you. The mount 'options' is a list of options,
// currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is
// required, call Mount with an empty string list or nil.
func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error {
return mounter.MountSensitive(source, target, fstype, options, nil)
}
// MountSensitive is the same as Mount() but this method allows
// sensitiveOptions to be passed in a separate parameter from the normal
// mount options and ensures the sensitiveOptions are never logged. This
// method should be used by callers that pass sensitive material (like
// passwords) as mount options.
func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
// Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty.
// All Linux distros are expected to be shipped with a mount utility that a support bind mounts.
mounterPath := ""
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
if bind {
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil)
if err != nil {
return err
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil)
}
// The list of filesystems that require containerized mounter on GCI image cluster
fsTypesNeedMounter := map[string]struct{}{
"nfs": {},
"glusterfs": {},
"ceph": {},
"cifs": {},
}
if _, ok := fsTypesNeedMounter[fstype]; ok {
mounterPath = mounter.mounterPath
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, nil)
}
func (mounter *Mounter) MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error {
// Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty.
// All Linux distros are expected to be shipped with a mount utility that a support bind mounts.
mounterPath := ""
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
if bind {
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin)
if err != nil {
return err
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin)
}
// The list of filesystems that require containerized mounter on GCI image cluster
fsTypesNeedMounter := map[string]struct{}{
"nfs": {},
"glusterfs": {},
"ceph": {},
"cifs": {},
}
if _, ok := fsTypesNeedMounter[fstype]; ok {
mounterPath = mounter.mounterPath
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, stdin)
}
// MountSensitiveWithoutSystemd is the same as MountSensitive() but disable using systemd mount.
func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
return mounter.MountSensitiveWithoutSystemdWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */)
}
// MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd with additional mount flags.
func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error {
mounterPath := ""
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
if bind {
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, mountFlags, false, nil)
if err != nil {
return err
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, mountFlags, false, nil)
}
// The list of filesystems that require containerized mounter on GCI image cluster
fsTypesNeedMounter := map[string]struct{}{
"nfs": {},
"glusterfs": {},
"ceph": {},
"cifs": {},
}
if _, ok := fsTypesNeedMounter[fstype]; ok {
mounterPath = mounter.mounterPath
}
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, mountFlags, false, nil)
}
// doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used.
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
// systemdMountRequired is an extension of option to decide whether uses systemd mount.
func (mounter *Mounter) doMount(mounterPath string, mountCmd string, source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string, systemdMountRequired bool, stdin []string) error {
mountArgs, mountArgsLogStr := MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, mountFlags)
if len(mounterPath) > 0 {
mountArgs = append([]string{mountCmd}, mountArgs...)
mountArgsLogStr = mountCmd + " " + mountArgsLogStr
mountCmd = mounterPath
}
if systemdMountRequired && mounter.hasSystemd() {
// Try to run mount via systemd-run --scope. This will escape the
// service where kubelet runs and any fuse daemons will be started in a
// specific scope. kubelet service than can be restarted without killing
// these fuse daemons.
//
// Complete command line (when mounterPath is not used):
// systemd-run --description=... --scope -- mount -t <type> <what> <where>
//
// Expected flow:
// * systemd-run creates a transient scope (=~ cgroup) and executes its
// argument (/bin/mount) there.
// * mount does its job, forks a fuse daemon if necessary and finishes.
// (systemd-run --scope finishes at this point, returning mount's exit
// code and stdout/stderr - thats one of --scope benefits).
// * systemd keeps the fuse daemon running in the scope (i.e. in its own
// cgroup) until the fuse daemon dies (another --scope benefit).
// Kubelet service can be restarted and the fuse daemon survives.
// * When the fuse daemon dies (e.g. during unmount) systemd removes the
// scope automatically.
//
// systemd-mount is not used because it's too new for older distros
// (CentOS 7, Debian Jessie).
mountCmd, mountArgs, mountArgsLogStr = AddSystemdScopeSensitive("systemd-run", target, mountCmd, mountArgs, mountArgsLogStr)
// } else {
// No systemd-run on the host (or we failed to check it), assume kubelet
// does not run as a systemd service.
// No code here, mountCmd and mountArgs are already populated.
}
// Logging with sensitive mount options removed.
klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgsLogStr)
command := exec.Command(mountCmd, mountArgs...)
if stdin != nil {
writer, err := command.StdinPipe()
if err != nil {
klog.Errorf("Create stdin pipe failed: %v\nMounting command: %s\nMounting arguments: %s\n", err, mountCmd, mountArgsLogStr)
return fmt.Errorf("create stdin pip failed: %v\nMounting command: %s\nMounting arguments: %s", err, mountCmd, mountArgsLogStr)
}
for _, v := range stdin {
io.WriteString(writer, v)
io.WriteString(writer, "\n")
}
writer.Close()
}
output, err := command.CombinedOutput()
if err != nil {
if err.Error() == errNoChildProcesses {
if command.ProcessState.Success() {
// We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753).
return nil
}
// Rewrite err with the actual exit error of the process.
err = &exec.ExitError{ProcessState: command.ProcessState}
}
klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, mountArgsLogStr, string(output))
return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s",
err, mountCmd, mountArgsLogStr, string(output))
}
return err
}
// detectSystemd returns true if OS runs with systemd as init. When not sure
// (permission errors, ...), it returns false.
// There may be different ways how to detect systemd, this one makes sure that
// systemd-runs (needed by Mount()) works.
func detectSystemd() bool {
if _, err := exec.LookPath("systemd-run"); err != nil {
klog.V(2).Infof("Detected OS without systemd")
return false
}
// Try to run systemd-run --scope /bin/true, that should be enough
// to make sure that systemd is really running and not just installed,
// which happens when running in a container with a systemd-based image
// but with different pid 1.
cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true")
output, err := cmd.CombinedOutput()
if err != nil {
klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS")
klog.V(4).Infof("systemd-run output: %s, failed with: %v", string(output), err)
return false
}
klog.V(2).Infof("Detected OS with systemd")
return true
}
// detectSafeNotMountedBehavior returns true if the umount implementation replies "not mounted"
// when the specified path is not mounted. When not sure (permission errors, ...), it returns false.
// When possible, we will trust umount's message and avoid doing our own mount point checks.
// More info: https://github.com/util-linux/util-linux/blob/v2.2/mount/umount.c#L179
func detectSafeNotMountedBehavior() bool {
return detectSafeNotMountedBehaviorWithExec(utilexec.New())
}
// detectSafeNotMountedBehaviorWithExec is for testing with FakeExec.
func detectSafeNotMountedBehaviorWithExec(exec utilexec.Interface) bool {
// create a temp dir and try to umount it
path, err := os.MkdirTemp("", "kubelet-detect-safe-umount")
if err != nil {
klog.V(4).Infof("Cannot create temp dir to detect safe 'not mounted' behavior: %v", err)
return false
}
defer os.RemoveAll(path)
cmd := exec.Command("umount", path)
output, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(string(output), errNotMounted) {
klog.V(4).Infof("Detected umount with safe 'not mounted' behavior")
return true
}
klog.V(4).Infof("'umount %s' failed with: %v, output: %s", path, err, string(output))
}
klog.V(4).Infof("Detected umount with unsafe 'not mounted' behavior")
return false
}
// MakeMountArgs makes the arguments to the mount(8) command.
// options MUST not contain sensitive material (like passwords).
func MakeMountArgs(source, target, fstype string, options []string) (mountArgs []string) {
mountArgs, _ = MakeMountArgsSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
return mountArgs
}
// MakeMountArgsSensitive makes the arguments to the mount(8) command.
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
func MakeMountArgsSensitive(source, target, fstype string, options []string, sensitiveOptions []string) (mountArgs []string, mountArgsLogStr string) {
return MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */)
}
// MakeMountArgsSensitiveWithMountFlags makes the arguments to the mount(8) command.
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
// mountFlags are additional mount flags that are not related with the fstype
// and mount options
func MakeMountArgsSensitiveWithMountFlags(source, target, fstype string, options []string, sensitiveOptions []string, mountFlags []string) (mountArgs []string, mountArgsLogStr string) {
// Build mount command as follows:
// mount [$mountFlags] [-t $fstype] [-o $options] [$source] $target
mountArgs = []string{}
mountArgsLogStr = ""
mountArgs = append(mountArgs, mountFlags...)
mountArgsLogStr += strings.Join(mountFlags, " ")
if len(fstype) > 0 {
mountArgs = append(mountArgs, "-t", fstype)
mountArgsLogStr += strings.Join(mountArgs, " ")
}
if len(options) > 0 || len(sensitiveOptions) > 0 {
combinedOptions := []string{}
combinedOptions = append(combinedOptions, options...)
combinedOptions = append(combinedOptions, sensitiveOptions...)
mountArgs = append(mountArgs, "-o", strings.Join(combinedOptions, ","))
// exclude sensitiveOptions from log string
mountArgsLogStr += " -o " + sanitizedOptionsForLogging(options, sensitiveOptions)
}
if len(source) > 0 {
mountArgs = append(mountArgs, source)
mountArgsLogStr += " " + source
}
mountArgs = append(mountArgs, target)
mountArgsLogStr += " " + target
return mountArgs, mountArgsLogStr
}
// AddSystemdScope adds "system-run --scope" to given command line
// If args contains sensitive material, use AddSystemdScopeSensitive to construct
// a safe to log string.
func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) {
descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName)
systemdRunArgs := []string{descriptionArg, "--scope", "--", command}
return systemdRunPath, append(systemdRunArgs, args...)
}
// AddSystemdScopeSensitive adds "system-run --scope" to given command line
// It also accepts takes a sanitized string containing mount arguments, mountArgsLogStr,
// and returns the string appended to the systemd command for logging.
func AddSystemdScopeSensitive(systemdRunPath, mountName, command string, args []string, mountArgsLogStr string) (string, []string, string) {
descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName)
systemdRunArgs := []string{descriptionArg, "--scope", "--", command}
return systemdRunPath, append(systemdRunArgs, args...), strings.Join(systemdRunArgs, " ") + " " + mountArgsLogStr
}
// Unmount unmounts the target.
// If the mounter has safe "not mounted" behavior, no error will be returned when the target is not a mount point.
func (mounter *Mounter) Unmount(target string) error {
klog.V(4).Infof("Unmounting %s", target)
command := exec.Command("umount", target)
output, err := command.CombinedOutput()
if err != nil {
return checkUmountError(target, command, output, err, mounter.withSafeNotMountedBehavior)
}
return nil
}
// UnmountWithForce unmounts given target but will retry unmounting with force option
// after given timeout.
func (mounter *Mounter) UnmountWithForce(target string, umountTimeout time.Duration) error {
err := tryUnmount(target, mounter.withSafeNotMountedBehavior, umountTimeout)
if err != nil {
if err == context.DeadlineExceeded {
klog.V(2).Infof("Timed out waiting for unmount of %s, trying with -f", target)
err = forceUmount(target, mounter.withSafeNotMountedBehavior)
}
return err
}
return nil
}
// List returns a list of all mounted filesystems.
func (*Mounter) List() ([]MountPoint, error) {
return ListProcMounts(procMountsPath)
}
// IsLikelyNotMountPoint determines if a directory is not a mountpoint.
// It is fast but not necessarily ALWAYS correct. If the path is in fact
// a bind mount from one part of a mount to another it will not be detected.
// It also can not distinguish between mountpoints and symbolic links.
// mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b")
// will return true. When in fact /tmp/b is a mount point. If this situation
// is of interest to you, don't use this function...
func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) {
stat, err := os.Stat(file)
if err != nil {
return true, err
}
rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/")))
if err != nil {
return true, err
}
// If the directory has a different device as parent, then it is a mountpoint.
if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev {
return false, nil
}
return true, nil
}
// CanSafelySkipMountPointCheck relies on the detected behavior of umount when given a target that is not a mount point.
func (mounter *Mounter) CanSafelySkipMountPointCheck() bool {
return mounter.withSafeNotMountedBehavior
}
// GetMountRefs finds all mount references to pathname, returns a
// list of paths. Path could be a mountpoint or a normal
// directory (for bind mount).
func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) {
pathExists, pathErr := PathExists(pathname)
if !pathExists {
return []string{}, nil
} else if IsCorruptedMnt(pathErr) {
klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname)
return []string{}, nil
} else if pathErr != nil {
return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr)
}
realpath, err := filepath.EvalSymlinks(pathname)
if err != nil {
return nil, err
}
return SearchMountPoints(realpath, procMountInfoPath)
}
// checkAndRepairFileSystem checks and repairs filesystems using command fsck.
func (mounter *SafeFormatAndMount) checkAndRepairFilesystem(source string) error {
klog.V(4).Infof("Checking for issues with fsck on disk: %s", source)
args := []string{"-a", source}
out, err := mounter.Exec.Command("fsck", args...).CombinedOutput()
if err != nil {
ee, isExitError := err.(utilexec.ExitError)
switch {
case err == utilexec.ErrExecutableNotFound:
klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.")
case isExitError && ee.ExitStatus() == fsckErrorsCorrected:
klog.Infof("Device %s has errors which were corrected by fsck.", source)
case isExitError && ee.ExitStatus() == fsckErrorsUncorrected:
return NewMountError(HasFilesystemErrors, "'fsck' found errors on device %s but could not correct them: %s", source, string(out))
case isExitError && ee.ExitStatus() > fsckErrorsUncorrected:
klog.Infof("`fsck` error %s", string(out))
default:
klog.Warningf("fsck on device %s failed with error %v, output: %v", source, err, string(out))
}
}
return nil
}
// formatAndMount uses unix utils to format and mount the given disk
func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error {
readOnly := false
for _, option := range options {
if option == "ro" {
readOnly = true
break
}
}
if !readOnly {
// Check sensitiveOptions for ro
for _, option := range sensitiveOptions {
if option == "ro" {
readOnly = true
break
}
}
}
options = append(options, "defaults")
mountErrorValue := UnknownMountError
// Check if the disk is already formatted
existingFormat, err := mounter.GetDiskFormat(source)
if err != nil {
return NewMountError(GetDiskFormatFailed, "failed to get disk format of disk %s: %v", source, err)
}
// Use 'ext4' as the default
if len(fstype) == 0 {
fstype = "ext4"
}
if existingFormat == "" {
// Do not attempt to format the disk if mounting as readonly, return an error to reflect this.
if readOnly {
return NewMountError(UnformattedReadOnly, "cannot mount unformatted disk %s as we are manipulating it in read-only mode", source)
}
// Disk is unformatted so format it.
args := []string{source}
if fstype == "ext4" || fstype == "ext3" {
args = []string{
"-F", // Force flag
"-m0", // Zero blocks reserved for super-user
source,
}
} else if fstype == "xfs" {
args = []string{
"-f", // force flag
source,
}
}
args = append(formatOptions, args...)
klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args)
output, err := mounter.format(fstype, args)
if err != nil {
// Do not log sensitiveOptions only options
sensitiveOptionsLog := sanitizedOptionsForLogging(options, sensitiveOptions)
detailedErr := fmt.Sprintf("format of disk %q failed: type:(%q) target:(%q) options:(%q) errcode:(%v) output:(%v) ", source, fstype, target, sensitiveOptionsLog, err, string(output))
klog.Error(detailedErr)
return NewMountError(FormatFailed, detailedErr)
}
klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target)
} else {
if fstype != existingFormat {
// Verify that the disk is formatted with filesystem type we are expecting
mountErrorValue = FilesystemMismatch
klog.Warningf("Configured to mount disk %s as %s but current format is %s, things might break", source, existingFormat, fstype)
}
if !readOnly {
// Run check tools on the disk to fix repairable issues, only do this for formatted volumes requested as rw.
err := mounter.checkAndRepairFilesystem(source)
if err != nil {
return err
}
}
}
// Mount the disk
klog.V(4).Infof("Attempting to mount disk %s in %s format at %s", source, fstype, target)
if err := mounter.MountSensitive(source, target, fstype, options, sensitiveOptions); err != nil {
return NewMountError(mountErrorValue, err.Error())
}
return nil
}
func (mounter *SafeFormatAndMount) format(fstype string, args []string) ([]byte, error) {
if mounter.formatSem != nil {
done := make(chan struct{})
defer close(done)
mounter.formatSem <- struct{}{}
go func() {
defer func() { <-mounter.formatSem }()
timeout := time.NewTimer(mounter.formatTimeout)
defer timeout.Stop()
select {
case <-done:
case <-timeout.C:
}
}()
}
return mounter.Exec.Command("mkfs."+fstype, args...).CombinedOutput()
}
func getDiskFormat(exec utilexec.Interface, disk string) (string, error) {
args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk}
klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args)
dataOut, err := exec.Command("blkid", args...).CombinedOutput()
output := string(dataOut)
klog.V(4).Infof("Output: %q", output)
if err != nil {
if exit, ok := err.(utilexec.ExitError); ok {
if exit.ExitStatus() == 2 {
// Disk device is unformatted.
// For `blkid`, if the specified token (TYPE/PTTYPE, etc) was
// not found, or no (specified) devices could be identified, an
// exit code of 2 is returned.
return "", nil
}
}
klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err)
return "", err
}
var fstype, pttype string
lines := strings.Split(output, "\n")
for _, l := range lines {
if len(l) <= 0 {
// Ignore empty line.
continue
}
cs := strings.Split(l, "=")
if len(cs) != 2 {
return "", fmt.Errorf("blkid returns invalid output: %s", output)
}
// TYPE is filesystem type, and PTTYPE is partition table type, according
// to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/.
if cs[0] == "TYPE" {
fstype = cs[1]
} else if cs[0] == "PTTYPE" {
pttype = cs[1]
}
}
if len(pttype) > 0 {
klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype)
// Returns a special non-empty string as filesystem type, then kubelet
// will not format it.
return "unknown data, probably partitions", nil
}
return fstype, nil
}
// GetDiskFormat uses 'blkid' to see if the given disk is unformatted
func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) {
return getDiskFormat(mounter.Exec, disk)
}
// ListProcMounts is shared with NsEnterMounter
func ListProcMounts(mountFilePath string) ([]MountPoint, error) {
content, err := readMountInfo(mountFilePath)
if err != nil {
return nil, err
}
return parseProcMounts(content)
}
func parseProcMounts(content []byte) ([]MountPoint, error) {
out := []MountPoint{}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if line == "" {
// the last split() item is empty string following the last \n
continue
}
fields := strings.Fields(line)
if len(fields) != expectedNumFieldsPerLine {
// Do not log line in case it contains sensitive Mount options
return nil, fmt.Errorf("wrong number of fields (expected %d, got %d)", expectedNumFieldsPerLine, len(fields))
}
mp := MountPoint{
Device: fields[0],
Path: fields[1],
Type: fields[2],
Opts: strings.Split(fields[3], ","),
}
freq, err := strconv.Atoi(fields[4])
if err != nil {
return nil, err
}
mp.Freq = freq
pass, err := strconv.Atoi(fields[5])
if err != nil {
return nil, err
}
mp.Pass = pass
out = append(out, mp)
}
return out, nil
}
// SearchMountPoints finds all mount references to the source, returns a list of
// mountpoints.
// The source can be a mount point or a normal directory (bind mount). We
// didn't support device because there is no use case by now.
// Some filesystems may share a source name, e.g. tmpfs. And for bind mounting,
// it's possible to mount a non-root path of a filesystem, so we need to use
// root path and major:minor to represent mount source uniquely.
// This implementation is shared between Linux and NsEnterMounter
func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) {
mis, err := ParseMountInfo(mountInfoPath)
if err != nil {
return nil, err
}
mountID := 0
rootPath := ""
major := -1
minor := -1
// Finding the underlying root path and major:minor if possible.
// We need search in backward order because it's possible for later mounts
// to overlap earlier mounts.
for i := len(mis) - 1; i >= 0; i-- {
if hostSource == mis[i].MountPoint || PathWithinBase(hostSource, mis[i].MountPoint) {
// If it's a mount point or path under a mount point.
mountID = mis[i].ID
rootPath = filepath.Join(mis[i].Root, strings.TrimPrefix(hostSource, mis[i].MountPoint))
major = mis[i].Major
minor = mis[i].Minor
break
}
}
if rootPath == "" || major == -1 || minor == -1 {
return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource)
}
var refs []string
for i := range mis {
if mis[i].ID == mountID {
// Ignore mount entry for mount source itself.
continue
}
if mis[i].Root == rootPath && mis[i].Major == major && mis[i].Minor == minor {
refs = append(refs, mis[i].MountPoint)
}
}
return refs, nil
}
// IsMountPoint determines if a file is a mountpoint.
// It first detects bind & any other mountpoints using
// MountedFast function. If the MountedFast function returns
// sure as true and err as nil, then a mountpoint is detected
// successfully. When an error is returned by MountedFast, the
// following is true:
// 1. All errors are returned with IsMountPoint as false
// except os.IsPermission.
// 2. When os.IsPermission is returned by MountedFast, List()
// is called to confirm if the given file is a mountpoint are not.
//
// os.ErrNotExist should always be returned if a file does not exist
// as callers have in past relied on this error and not fallback.
//
// When MountedFast returns sure as false and err as nil (eg: in
// case of bindmounts on kernel version 5.10- ); mounter.List()
// endpoint is called to enumerate all the mountpoints and check if
// it is mountpoint match or not.
func (mounter *Mounter) IsMountPoint(file string) (bool, error) {
isMnt, sure, isMntErr := mountinfo.MountedFast(file)
if sure && isMntErr == nil {
return isMnt, nil
}
if isMntErr != nil {
if errors.Is(isMntErr, fs.ErrNotExist) {
return false, fs.ErrNotExist
}
// We were not allowed to do the simple stat() check, e.g. on NFS with
// root_squash. Fall back to /proc/mounts check below when
// fs.ErrPermission is returned.
if !errors.Is(isMntErr, fs.ErrPermission) {
return false, isMntErr
}
}
// Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts.
resolvedFile, err := filepath.EvalSymlinks(file)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return false, fs.ErrNotExist
}
return false, err
}
// check all mountpoints since MountedFast is not sure.
// is not reliable for some mountpoint types.
mountPoints, mountPointsErr := mounter.List()
if mountPointsErr != nil {
return false, mountPointsErr
}
for _, mp := range mountPoints {
if isMountPointMatch(mp, resolvedFile) {
return true, nil
}
}
return false, nil
}
// tryUnmount calls plain "umount" and waits for unmountTimeout for it to finish.
func tryUnmount(target string, withSafeNotMountedBehavior bool, unmountTimeout time.Duration) error {
klog.V(4).Infof("Unmounting %s", target)
ctx, cancel := context.WithTimeout(context.Background(), unmountTimeout)
defer cancel()
command := exec.CommandContext(ctx, "umount", target)
output, err := command.CombinedOutput()
// CombinedOutput() does not return DeadlineExceeded, make sure it's
// propagated on timeout.
if ctx.Err() != nil {
return ctx.Err()
}
if err != nil {
return checkUmountError(target, command, output, err, withSafeNotMountedBehavior)
}
return nil
}
func forceUmount(target string, withSafeNotMountedBehavior bool) error {
command := exec.Command("umount", "-f", target)
output, err := command.CombinedOutput()
if err != nil {
return checkUmountError(target, command, output, err, withSafeNotMountedBehavior)
}
return nil
}
// checkUmountError checks a result of umount command and determine a return value.
func checkUmountError(target string, command *exec.Cmd, output []byte, err error, withSafeNotMountedBehavior bool) error {
if err.Error() == errNoChildProcesses {
if command.ProcessState.Success() {
// We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753).
return nil
}
// Rewrite err with the actual exit error of the process.
err = &exec.ExitError{ProcessState: command.ProcessState}
}
if withSafeNotMountedBehavior && strings.Contains(string(output), errNotMounted) {
klog.V(4).Infof("ignoring 'not mounted' error for %s", target)
return nil
}
return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output))
}

@ -0,0 +1,166 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"context"
"os"
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"k8s.io/klog/v2"
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
)
type NodeServer struct {
csi.UnimplementedNodeServer
Driver *Driver
mounter mount.Interface
}
func NewNodeServer(d *Driver, mounter mount.Interface) *NodeServer {
return &NodeServer{
Driver: d,
mounter: mounter,
}
}
// NodePublishVolume implements csi.NodeServer.
func (n *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
volCap := req.GetVolumeCapability()
if volCap == nil {
return nil, status.Error(codes.InvalidArgument, "Volume capability missing in request")
}
volumeID := req.GetVolumeId()
if len(volumeID) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
}
targetPath := req.GetTargetPath()
if len(targetPath) == 0 {
return nil, status.Error(codes.InvalidArgument, "Target path not provided")
}
mountOptions := volCap.GetMount().GetMountFlags()
if req.GetReadonly() {
mountOptions = append(mountOptions, "ro")
}
address, subDir, err := ParseVolumeId(volumeID)
if err != nil {
// An invalid ID should be treated as doesn't exist
klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err)
return &csi.NodePublishVolumeResponse{}, nil
}
notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath)
if err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(targetPath, os.FileMode(n.Driver.mountPermissions)); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
notMnt = true
} else {
return nil, status.Error(codes.Internal, err.Error())
}
}
if !notMnt {
return &csi.NodePublishVolumeResponse{}, nil
}
sourcePath := strings.Join([]string{address, subDir}, "/")
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
klog.V(2).Infof("NodePublishVolume: volumeID(%v) source(%s) targetPath(%s) mountflags(%v)", volumeID, sourcePath, targetPath, mountOptions)
err = n.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, mountOptions, nil, stdin)
if err != nil {
if os.IsPermission(err) {
return nil, status.Error(codes.PermissionDenied, err.Error())
}
if strings.Contains(err.Error(), "invalid argument") {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
return &csi.NodePublishVolumeResponse{}, nil
}
// NodeUnpublishVolume implements csi.NodeServer.
func (n *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
volumeID := req.GetVolumeId()
if len(volumeID) == 0 {
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
}
targetPath := req.GetTargetPath()
if len(targetPath) == 0 {
return nil, status.Error(codes.InvalidArgument, "Target path missing in request")
}
notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath)
if err != nil {
if os.IsNotExist(err) {
return nil, status.Error(codes.NotFound, "Targetpath not found")
}
return nil, status.Error(codes.Internal, err.Error())
}
if notMnt {
return &csi.NodeUnpublishVolumeResponse{}, nil
}
klog.V(2).Infof("NodeUnpublishVolume: unmounting volume %s on %s", volumeID, targetPath)
err = n.mounter.Unmount(targetPath)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to unmount target %q: %v", targetPath, err)
}
klog.V(2).Infof("NodeUnpublishVolume: unmount volume %s on %s successfully", volumeID, targetPath)
return &csi.NodeUnpublishVolumeResponse{}, nil
}
// NodeGetInfo implements csi.NodeServer.
func (n *NodeServer) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
return &csi.NodeGetInfoResponse{NodeId: n.Driver.nodeID}, nil
}
// NodeGetCapabilities implements csi.NodeServer.
func (n *NodeServer) NodeGetCapabilities(context.Context, *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
return &csi.NodeGetCapabilitiesResponse{
Capabilities: n.Driver.nscap,
}, nil
}
// NodeExpandVolume implements csi.NodeServer.
func (*NodeServer) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// NodeGetVolumeStats implements csi.NodeServer.
func (*NodeServer) NodeGetVolumeStats(context.Context, *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// NodeStageVolume implements csi.NodeServer.
func (*NodeServer) NodeStageVolume(context.Context, *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}
// NodeUnstageVolume implements csi.NodeServer.
func (*NodeServer) NodeUnstageVolume(context.Context, *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
return nil, status.Error(codes.Unimplemented, "")
}

@ -0,0 +1,108 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"net"
"os"
"sync"
"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc"
"k8s.io/klog/v2"
)
// Defines Non blocking GRPC server interfaces
type NonBlockingGRPCServer interface {
// Start services at the endpoint
Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer)
// Waits for the service to stop
Wait()
// Stops the service gracefully
Stop()
// Stops the service forcefully
ForceStop()
}
func NewNonBlockingGRPCServer() NonBlockingGRPCServer {
return &nonBlockingGRPCServer{}
}
// NonBlocking server
type nonBlockingGRPCServer struct {
wg sync.WaitGroup
server *grpc.Server
}
func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
s.wg.Add(1)
go s.serve(endpoint, ids, cs, ns)
}
func (s *nonBlockingGRPCServer) Wait() {
s.wg.Wait()
}
func (s *nonBlockingGRPCServer) Stop() {
s.server.GracefulStop()
}
func (s *nonBlockingGRPCServer) ForceStop() {
s.server.Stop()
}
func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
proto, addr, err := ParseEndpoint(endpoint)
if err != nil {
klog.Fatal(err.Error())
}
if proto == "unix" {
addr = "/" + addr
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
klog.Fatalf("Failed to remove %s, error: %s", addr, err.Error())
}
}
listener, err := net.Listen(proto, addr)
if err != nil {
klog.Fatalf("Failed to listen: %v", err)
}
opts := []grpc.ServerOption{
grpc.UnaryInterceptor(logGRPC),
}
server := grpc.NewServer(opts...)
s.server = server
if ids != nil {
csi.RegisterIdentityServer(server, ids)
}
if cs != nil {
csi.RegisterControllerServer(server, cs)
}
if ns != nil {
csi.RegisterNodeServer(server, ns)
}
klog.Infof("Listening for connections on address: %#v", listener.Addr())
err = server.Serve(listener)
if err != nil {
klog.Fatalf("Failed to serve grpc server: %v", err)
}
}

@ -0,0 +1,93 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"context"
"errors"
"fmt"
"strings"
"github.com/container-storage-interface/spec/lib/go/csi"
"google.golang.org/grpc"
"k8s.io/klog/v2"
)
func ParseEndpoint(ep string) (string, string, error) {
if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") {
s := strings.SplitN(ep, "://", 2)
if s[1] != "" {
return s[0], s[1], nil
}
}
return "", "", fmt.Errorf("invalid endpoint: %v", ep)
}
func getLogLevel(method string) int32 {
if method == "/csi.v1.Identity/Probe" ||
method == "/csi.v1.Node/NodeGetCapabilities" ||
method == "/csi.v1.Node/NodeGetVolumeStats" {
return 8
}
return 2
}
func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
level := klog.Level(getLogLevel(info.FullMethod))
klog.V(level).Infof("GRPC call: %s", info.FullMethod)
klog.V(level).Infof("GRPC request: %s", req)
resp, err := handler(ctx, req)
if err != nil {
klog.Errorf("GRPC error: %v", err)
} else {
klog.V(level).Infof("GRPC response: %s", resp)
}
return resp, err
}
func NewControllerServiceCapability(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability {
return &csi.ControllerServiceCapability{
Type: &csi.ControllerServiceCapability_Rpc{
Rpc: &csi.ControllerServiceCapability_RPC{
Type: cap,
},
},
}
}
func NewNodeServiceCapability(cap csi.NodeServiceCapability_RPC_Type) *csi.NodeServiceCapability {
return &csi.NodeServiceCapability{
Type: &csi.NodeServiceCapability_Rpc{
Rpc: &csi.NodeServiceCapability_RPC{
Type: cap,
},
},
}
}
func MakeVolumeId(webdavSharePath, volumeName string) string {
return fmt.Sprintf("%s#%s", webdavSharePath, volumeName)
}
func ParseVolumeId(volumeId string) (webdavSharePath, subDir string, err error) {
arr := strings.Split(volumeId, "#")
if len(arr) < 2 {
return "", "", errors.New("invalid volumeId")
}
return arr[0], arr[1], nil
}

@ -0,0 +1,66 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webdav
import (
"fmt"
"runtime"
"strings"
"sigs.k8s.io/yaml"
)
// These are set during build time via -ldflags
var (
driverVersion = "N/A"
gitCommit = "N/A"
buildDate = "N/A"
)
// VersionInfo holds the version information of the driver
type VersionInfo struct {
DriverName string `json:"Driver Name"`
DriverVersion string `json:"Driver Version"`
GitCommit string `json:"Git Commit"`
BuildDate string `json:"Build Date"`
GoVersion string `json:"Go Version"`
Compiler string `json:"Compiler"`
Platform string `json:"Platform"`
}
// GetVersion returns the version information of the driver
func GetVersion(driverName string) VersionInfo {
return VersionInfo{
DriverName: driverName,
DriverVersion: driverVersion,
GitCommit: gitCommit,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}
}
// GetVersionYAML returns the version information of the driver
func GetVersionYAML(driverName string) (string, error) {
info := GetVersion(driverName)
marshalled, err := yaml.Marshal(&info)
if err != nil {
return "", err
}
return strings.TrimSpace(string(marshalled)), nil
}

@ -0,0 +1 @@
kind create cluster --config=kind.yaml

@ -0,0 +1,18 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.29.0
extraMounts:
- hostPath: /root/workspace/csi-driver-webdav/test/csi
containerPath: /csi
networking:
apiServerPort: 6443
podSubnet: 172.16.0.0/16
serviceSubnet: 172.19.0.0/16
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://hub-mirror.c.163.com"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
endpoint = ["http://registry:5000"]
Loading…
Cancel
Save