├── test ├── kind.sh └── kind.yaml ├── examples ├── csi-webdav-secret.yaml ├── csi-webdav-dynamic-pvc.yaml ├── csi-webdav-pod.yaml └── csi-webdav-storageclass.yaml ├── deploy ├── csi-webdav-driverinfo.yaml ├── csi-webdav-rbac.yaml ├── csi-webdav-controller.yaml └── csi-webdav-node.yaml ├── .gitlab-ci.yml ├── Makefile ├── Dockerfile ├── go.mod ├── README.md ├── .gitignore ├── cmd └── webdav │ └── main.go ├── pkg └── webdav │ ├── version.go │ ├── identity.go │ ├── utils.go │ ├── server.go │ ├── driver.go │ ├── mount │ ├── mount_helper_common.go │ ├── mount_helper_unix.go │ ├── mount.go │ └── mount_linux.go │ ├── node.go │ └── controller.go ├── go.sum └── LICENSE /test/kind.sh: -------------------------------------------------------------------------------- 1 | kind create cluster --config=kind.yaml -------------------------------------------------------------------------------- /examples/csi-webdav-secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: webdav-secrect 6 | type: Opaque 7 | data: 8 | username: YWRtaW4= 9 | password: YWRtaW4= -------------------------------------------------------------------------------- /deploy/csi-webdav-driverinfo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: storage.k8s.io/v1 3 | kind: CSIDriver 4 | metadata: 5 | name: webdav.csi.io 6 | spec: 7 | attachRequired: false 8 | volumeLifecycleModes: 9 | - Persistent -------------------------------------------------------------------------------- /examples/csi-webdav-dynamic-pvc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: pvc-webdav-dynamic 6 | spec: 7 | accessModes: 8 | - ReadWriteMany 9 | resources: 10 | requests: 11 | storage: 10Gi 12 | storageClassName: webdav-sc -------------------------------------------------------------------------------- /examples/csi-webdav-pod.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Pod 4 | metadata: 5 | name: nginx 6 | spec: 7 | containers: 8 | - name: nginx 9 | image: nginx:latest 10 | imagePullPolicy: IfNotPresent 11 | volumeMounts: 12 | - name: pvc-webdav-dynamic 13 | mountPath: /var/www/html 14 | volumes: 15 | - name: pvc-webdav-dynamic 16 | persistentVolumeClaim: 17 | claimName: pvc-webdav-dynamic -------------------------------------------------------------------------------- /examples/csi-webdav-storageclass.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: storage.k8s.io/v1 3 | kind: StorageClass 4 | metadata: 5 | name: webdav-sc 6 | provisioner: webdav.csi.io 7 | parameters: 8 | # alist folder webdav address 9 | share: http://ip:port/dav/media 10 | csi.storage.k8s.io/provisioner-secret-name: "webdav-secrect" 11 | csi.storage.k8s.io/provisioner-secret-namespace: "default" 12 | csi.storage.k8s.io/node-publish-secret-name: "webdav-secrect" 13 | csi.storage.k8s.io/node-publish-secret-namespace: "default" 14 | reclaimPolicy: Delete 15 | volumeBindingMode: Immediate 16 | mountOptions: -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - package 4 | 5 | build go binary: 6 | stage: build 7 | image: golang:1.24-bookworm 8 | script: 9 | - make go-build 10 | artifacts: 11 | paths: 12 | - bin/webdavplugin 13 | 14 | build docker image: 15 | stage: package 16 | image: 17 | name: gcr.io/kaniko-project/executor:v1.23.2-debug 18 | entrypoint: [""] 19 | script: 20 | - /kaniko/executor 21 | --context "${CI_PROJECT_DIR}" 22 | --dockerfile "${CI_PROJECT_DIR}/Dockerfile" 23 | --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}" 24 | rules: 25 | - if: $CI_COMMIT_TAG 26 | -------------------------------------------------------------------------------- /test/kind.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | image: kindest/node:v1.29.0 6 | extraMounts: 7 | - hostPath: /root/workspace/csi-driver-webdav/test/csi 8 | containerPath: /csi 9 | networking: 10 | apiServerPort: 6443 11 | podSubnet: 172.16.0.0/16 12 | serviceSubnet: 172.19.0.0/16 13 | containerdConfigPatches: 14 | - |- 15 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] 16 | endpoint = ["https://hub-mirror.c.163.com"] 17 | [plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"] 18 | endpoint = ["http://registry:5000"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_COMMIT = $(shell git rev-parse HEAD) 2 | BUILD_DATE = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 3 | 4 | PKG = github.com/sys-liqian/csi-driver-webdav 5 | LDFLAGS = -X ${PKG}/pkg/webdav.driverVersion=${IMAGE_VERSION} -X ${PKG}/pkg/webdav.gitCommit=${GIT_COMMIT} -X ${PKG}/pkg/webdav.buildDate=${BUILD_DATE} 6 | EXT_LDFLAGS = -s -w -extldflags "-static" 7 | 8 | IMAGE_VERSION ?= v0.0.1 9 | LOCAL_REPOSITORY ?= localhost:5000 10 | 11 | .PHONY: go-build 12 | go-build: 13 | CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) go build -a -ldflags "${LDFLAGS} ${EXT_LDFLAGS}" -o bin/webdavplugin ./cmd/webdav 14 | 15 | .PHONY: docker-build 16 | docker-build: go-build 17 | docker build --network host -t $(LOCAL_REPOSITORY)/webdavplugin:$(IMAGE_VERSION) . -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023. 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | FROM debian:bookworm 16 | 17 | ARG binary=./bin/webdavplugin 18 | COPY ${binary} /webdavplugin 19 | 20 | RUN apt update && apt install -y davfs2 21 | 22 | ENTRYPOINT ["/webdavplugin"] 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sys-liqian/csi-driver-webdav 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/container-storage-interface/spec v1.11.0 9 | github.com/golang/protobuf v1.5.4 10 | github.com/moby/sys/mountinfo v0.7.2 11 | golang.org/x/sys v0.31.0 12 | google.golang.org/grpc v1.71.0 13 | k8s.io/klog/v2 v2.130.1 14 | k8s.io/utils v0.0.0-20241210054802-24370beab758 15 | sigs.k8s.io/yaml v1.4.0 16 | ) 17 | 18 | require ( 19 | github.com/go-logr/logr v1.4.2 // indirect 20 | github.com/kr/pretty v0.3.1 // indirect 21 | github.com/rogpeppe/go-internal v1.10.0 // indirect 22 | golang.org/x/net v0.37.0 // indirect 23 | golang.org/x/text v0.23.0 // indirect 24 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect 25 | google.golang.org/protobuf v1.36.5 // indirect 26 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webdav CSI driver for Kubernetes 2 | 3 | ### Overview 4 | 5 | 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. 6 | 7 | ### Quick start with kind 8 | 9 | #### Build plugin image 10 | ```bash 11 | make docker-build 12 | ``` 13 | 14 | #### Start kind cluster 15 | ```bash 16 | kind create cluster --image kindest/node:v1.27.3 17 | ``` 18 | 19 | ### Load plugin image to kind cluster 20 | ```bash 21 | kind load docker-image registry.k8s.io/sig-storage/csi-provisioner:v3.6.2 22 | kind load docker-image registry.k8s.io/sig-storage/livenessprobe:v2.11.0 23 | kind load docker-image registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.1 24 | kind load docker-image localhost:5000/webdavplugin:v0.0.1 25 | ``` 26 | 27 | ### Deploy CSI 28 | ```bash 29 | kubectl apply -f deploy/ 30 | ``` 31 | 32 | ### Tests 33 | ```bash 34 | kubectl apply -f examples/csi-webdav-secret.yaml 35 | kubectl apply -f examples/csi-webdav-storageclass.yaml 36 | kubectl apply -f examples/csi-webdav-dynamic-pvc.yaml 37 | kubectl apply -f examples/csi-webdav-pod.yaml 38 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX leaves these everywhere on SMB shares 2 | ._* 3 | 4 | # OSX trash 5 | .DS_Store 6 | 7 | # Eclipse files 8 | .classpath 9 | .project 10 | .settings/** 11 | 12 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 13 | .idea/ 14 | *.iml 15 | 16 | # Vscode files 17 | .vscode 18 | 19 | # This is where the result of the go build goes 20 | /output*/ 21 | /_output*/ 22 | /_output 23 | /bin 24 | 25 | # Emacs save files 26 | *~ 27 | \#*\# 28 | .\#* 29 | 30 | # Vim-related files 31 | [._]*.s[a-w][a-z] 32 | [._]s[a-w][a-z] 33 | *.un~ 34 | Session.vim 35 | .netrwhist 36 | 37 | # cscope-related files 38 | cscope.* 39 | 40 | # Go test binaries 41 | *.test 42 | 43 | # JUnit test output from ginkgo e2e tests 44 | /junit*.xml 45 | 46 | # Mercurial files 47 | **/.hg 48 | **/.hg* 49 | 50 | # Vagrant 51 | .vagrant 52 | 53 | .tags* 54 | 55 | # Test artifacts produced by Jenkins jobs 56 | /_artifacts/ 57 | 58 | # Go dependencies installed on Jenkins 59 | /_gopath/ 60 | 61 | # direnv .envrc files 62 | .envrc 63 | 64 | # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored 65 | !\.drone\.sec 66 | 67 | # Godeps or dep workspace 68 | /Godeps/_workspace 69 | 70 | /bazel-* 71 | *.pyc 72 | profile.cov -------------------------------------------------------------------------------- /deploy/csi-webdav-rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: webdav-csi-sa 6 | namespace: kube-system 7 | --- 8 | kind: ClusterRole 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | metadata: 11 | name: webdav-csi-cr 12 | rules: 13 | - apiGroups: [""] 14 | resources: ["persistentvolumes"] 15 | verbs: ["get", "list", "watch", "create", "delete"] 16 | - apiGroups: [""] 17 | resources: ["persistentvolumeclaims"] 18 | verbs: ["get", "list", "watch", "update"] 19 | - apiGroups: [""] 20 | resources: ["secrets"] 21 | verbs: ["get", "list", "watch"] 22 | - apiGroups: ["storage.k8s.io"] 23 | resources: ["storageclasses"] 24 | verbs: ["get", "list", "watch"] 25 | - apiGroups: [""] 26 | resources: ["events"] 27 | verbs: ["list", "watch", "create", "update", "patch"] 28 | - apiGroups: ["storage.k8s.io"] 29 | resources: ["csinodes"] 30 | verbs: ["get", "list", "watch"] 31 | - apiGroups: [""] 32 | resources: ["nodes"] 33 | verbs: ["get", "list", "watch"] 34 | - apiGroups: ["coordination.k8s.io"] 35 | resources: ["leases"] 36 | verbs: ["get", "watch", "list", "delete", "update", "create"] 37 | --- 38 | kind: ClusterRoleBinding 39 | apiVersion: rbac.authorization.k8s.io/v1 40 | metadata: 41 | name: webdav-csi-crb 42 | subjects: 43 | - kind: ServiceAccount 44 | name: webdav-csi-sa 45 | namespace: kube-system 46 | roleRef: 47 | kind: ClusterRole 48 | name: webdav-csi-cr 49 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /cmd/webdav/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | "github.com/sys-liqian/csi-driver-webdav/pkg/webdav" 24 | "k8s.io/klog/v2" 25 | ) 26 | 27 | var ( 28 | endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI endpoint") 29 | nodeID = flag.String("nodeid", "", "node id") 30 | mountPermissions = flag.Uint64("mount-permissions", 0, "mounted folder permissions") 31 | driverName = flag.String("drivername", "", "name of the driver") 32 | workingMountDir = flag.String("working-mount-dir", "/tmp/csi-storage", "working directory for provisioner to mount davfs shares temporarily") 33 | defaultOnDeletePolicy = flag.String("default-ondelete-policy", "", "default policy for deleting subdirectory when deleting a volume") 34 | ) 35 | 36 | func main() { 37 | klog.InitFlags(nil) 38 | _ = flag.Set("logtostderr", "true") 39 | flag.Parse() 40 | if *nodeID == "" { 41 | klog.Warning("nodeid is empty") 42 | } 43 | 44 | driverOptions := webdav.DriverOpt{ 45 | Name: *driverName, 46 | NodeID: *nodeID, 47 | Endpoint: *endpoint, 48 | MountPermissions: *mountPermissions, 49 | WorkingMountDir: *workingMountDir, 50 | DefaultOnDeletePolicy: *defaultOnDeletePolicy, 51 | } 52 | d := webdav.NewDriver(&driverOptions) 53 | d.Run() 54 | os.Exit(0) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/webdav/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | 24 | "sigs.k8s.io/yaml" 25 | ) 26 | 27 | // These are set during build time via -ldflags 28 | var ( 29 | driverVersion = "N/A" 30 | gitCommit = "N/A" 31 | buildDate = "N/A" 32 | ) 33 | 34 | // VersionInfo holds the version information of the driver 35 | type VersionInfo struct { 36 | DriverName string `json:"Driver Name"` 37 | DriverVersion string `json:"Driver Version"` 38 | GitCommit string `json:"Git Commit"` 39 | BuildDate string `json:"Build Date"` 40 | GoVersion string `json:"Go Version"` 41 | Compiler string `json:"Compiler"` 42 | Platform string `json:"Platform"` 43 | } 44 | 45 | // GetVersion returns the version information of the driver 46 | func GetVersion(driverName string) VersionInfo { 47 | return VersionInfo{ 48 | DriverName: driverName, 49 | DriverVersion: driverVersion, 50 | GitCommit: gitCommit, 51 | BuildDate: buildDate, 52 | GoVersion: runtime.Version(), 53 | Compiler: runtime.Compiler, 54 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 55 | } 56 | } 57 | 58 | // GetVersionYAML returns the version information of the driver 59 | func GetVersionYAML(driverName string) (string, error) { 60 | info := GetVersion(driverName) 61 | marshalled, err := yaml.Marshal(&info) 62 | if err != nil { 63 | return "", err 64 | } 65 | return strings.TrimSpace(string(marshalled)), nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/webdav/identity.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/container-storage-interface/spec/lib/go/csi" 23 | "github.com/golang/protobuf/ptypes/wrappers" 24 | "google.golang.org/grpc/codes" 25 | "google.golang.org/grpc/status" 26 | ) 27 | 28 | type IdentityServer struct { 29 | Driver *Driver 30 | csi.UnimplementedIdentityServer 31 | } 32 | 33 | func NewIdentityServer(d *Driver) *IdentityServer { 34 | return &IdentityServer{ 35 | Driver: d, 36 | } 37 | } 38 | 39 | func (ids *IdentityServer) GetPluginInfo(_ context.Context, _ *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { 40 | if ids.Driver.name == "" { 41 | return nil, status.Error(codes.Unavailable, "Driver name not configured") 42 | } 43 | 44 | if ids.Driver.version == "" { 45 | return nil, status.Error(codes.Unavailable, "Driver is missing version") 46 | } 47 | 48 | return &csi.GetPluginInfoResponse{ 49 | Name: ids.Driver.name, 50 | VendorVersion: ids.Driver.version, 51 | }, nil 52 | } 53 | 54 | func (ids *IdentityServer) Probe(_ context.Context, _ *csi.ProbeRequest) (*csi.ProbeResponse, error) { 55 | return &csi.ProbeResponse{Ready: &wrappers.BoolValue{Value: true}}, nil 56 | } 57 | 58 | func (ids *IdentityServer) GetPluginCapabilities(_ context.Context, _ *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) { 59 | return &csi.GetPluginCapabilitiesResponse{ 60 | Capabilities: []*csi.PluginCapability{ 61 | { 62 | Type: &csi.PluginCapability_Service_{ 63 | Service: &csi.PluginCapability_Service{ 64 | Type: csi.PluginCapability_Service_CONTROLLER_SERVICE, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/webdav/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/container-storage-interface/spec/lib/go/csi" 26 | "google.golang.org/grpc" 27 | "k8s.io/klog/v2" 28 | ) 29 | 30 | func ParseEndpoint(ep string) (string, string, error) { 31 | if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") { 32 | s := strings.SplitN(ep, "://", 2) 33 | if s[1] != "" { 34 | return s[0], s[1], nil 35 | } 36 | } 37 | return "", "", fmt.Errorf("invalid endpoint: %v", ep) 38 | } 39 | 40 | func getLogLevel(method string) int32 { 41 | if method == "/csi.v1.Identity/Probe" || 42 | method == "/csi.v1.Node/NodeGetCapabilities" || 43 | method == "/csi.v1.Node/NodeGetVolumeStats" { 44 | return 8 45 | } 46 | return 2 47 | } 48 | 49 | func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 50 | level := klog.Level(getLogLevel(info.FullMethod)) 51 | klog.V(level).Infof("GRPC call: %s", info.FullMethod) 52 | klog.V(level).Infof("GRPC request: %s", req) 53 | 54 | resp, err := handler(ctx, req) 55 | if err != nil { 56 | klog.Errorf("GRPC error: %v", err) 57 | } else { 58 | klog.V(level).Infof("GRPC response: %s", resp) 59 | } 60 | return resp, err 61 | } 62 | 63 | func NewControllerServiceCapability(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability { 64 | return &csi.ControllerServiceCapability{ 65 | Type: &csi.ControllerServiceCapability_Rpc{ 66 | Rpc: &csi.ControllerServiceCapability_RPC{ 67 | Type: cap, 68 | }, 69 | }, 70 | } 71 | } 72 | 73 | func NewNodeServiceCapability(cap csi.NodeServiceCapability_RPC_Type) *csi.NodeServiceCapability { 74 | return &csi.NodeServiceCapability{ 75 | Type: &csi.NodeServiceCapability_Rpc{ 76 | Rpc: &csi.NodeServiceCapability_RPC{ 77 | Type: cap, 78 | }, 79 | }, 80 | } 81 | } 82 | 83 | func MakeVolumeId(webdavSharePath, volumeName string) string { 84 | return fmt.Sprintf("%s#%s", webdavSharePath, volumeName) 85 | } 86 | 87 | func ParseVolumeId(volumeId string) (webdavSharePath, subDir string, err error) { 88 | arr := strings.Split(volumeId, "#") 89 | if len(arr) < 2 { 90 | return "", "", errors.New("invalid volumeId") 91 | } 92 | return arr[0], arr[1], nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/webdav/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "net" 21 | "os" 22 | "sync" 23 | 24 | "github.com/container-storage-interface/spec/lib/go/csi" 25 | "google.golang.org/grpc" 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | // Defines Non blocking GRPC server interfaces 30 | type NonBlockingGRPCServer interface { 31 | // Start services at the endpoint 32 | Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) 33 | // Waits for the service to stop 34 | Wait() 35 | // Stops the service gracefully 36 | Stop() 37 | // Stops the service forcefully 38 | ForceStop() 39 | } 40 | 41 | func NewNonBlockingGRPCServer() NonBlockingGRPCServer { 42 | return &nonBlockingGRPCServer{} 43 | } 44 | 45 | // NonBlocking server 46 | type nonBlockingGRPCServer struct { 47 | wg sync.WaitGroup 48 | server *grpc.Server 49 | } 50 | 51 | func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { 52 | s.wg.Add(1) 53 | go s.serve(endpoint, ids, cs, ns) 54 | } 55 | 56 | func (s *nonBlockingGRPCServer) Wait() { 57 | s.wg.Wait() 58 | } 59 | 60 | func (s *nonBlockingGRPCServer) Stop() { 61 | s.server.GracefulStop() 62 | } 63 | 64 | func (s *nonBlockingGRPCServer) ForceStop() { 65 | s.server.Stop() 66 | } 67 | 68 | func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) { 69 | proto, addr, err := ParseEndpoint(endpoint) 70 | if err != nil { 71 | klog.Fatal(err.Error()) 72 | } 73 | 74 | if proto == "unix" { 75 | addr = "/" + addr 76 | if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { 77 | klog.Fatalf("Failed to remove %s, error: %s", addr, err.Error()) 78 | } 79 | } 80 | 81 | listener, err := net.Listen(proto, addr) 82 | if err != nil { 83 | klog.Fatalf("Failed to listen: %v", err) 84 | } 85 | 86 | opts := []grpc.ServerOption{ 87 | grpc.UnaryInterceptor(logGRPC), 88 | } 89 | server := grpc.NewServer(opts...) 90 | s.server = server 91 | 92 | if ids != nil { 93 | csi.RegisterIdentityServer(server, ids) 94 | } 95 | if cs != nil { 96 | csi.RegisterControllerServer(server, cs) 97 | } 98 | if ns != nil { 99 | csi.RegisterNodeServer(server, ns) 100 | } 101 | 102 | klog.Infof("Listening for connections on address: %#v", listener.Addr()) 103 | 104 | err = server.Serve(listener) 105 | if err != nil { 106 | klog.Fatalf("Failed to serve grpc server: %v", err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/webdav/driver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "github.com/container-storage-interface/spec/lib/go/csi" 21 | "github.com/sys-liqian/csi-driver-webdav/pkg/webdav/mount" 22 | 23 | "k8s.io/klog/v2" 24 | ) 25 | 26 | const ( 27 | DefaultDriverName = "webdav.csi.io" 28 | fstype = "davfs" 29 | webdavSharePath = "share" 30 | mountPermissionsField = "mountpermissions" 31 | pvcNameKey = "csi.storage.k8s.io/pvc/name" 32 | pvcNamespaceKey = "csi.storage.k8s.io/pvc/namespace" 33 | pvNameKey = "csi.storage.k8s.io/pv/name" 34 | secretUsernameKey = "username" 35 | secretPasswordKey = "password" 36 | ) 37 | 38 | type Driver struct { 39 | name string 40 | nodeID string 41 | endpoint string 42 | version string 43 | mountPermissions uint64 44 | workingMountDir string 45 | defaultOnDeletePolicy string 46 | 47 | cscap []*csi.ControllerServiceCapability 48 | nscap []*csi.NodeServiceCapability 49 | } 50 | 51 | type DriverOpt struct { 52 | Name string 53 | NodeID string 54 | Endpoint string 55 | MountPermissions uint64 56 | WorkingMountDir string 57 | DefaultOnDeletePolicy string 58 | } 59 | 60 | func NewDriver(opt *DriverOpt) *Driver { 61 | klog.V(2).Infof("Driver: %v version: %v", opt.Name, driverVersion) 62 | 63 | driverName := opt.Name 64 | if driverName == "" { 65 | driverName = DefaultDriverName 66 | } 67 | 68 | driver := &Driver{ 69 | name: driverName, 70 | nodeID: opt.NodeID, 71 | endpoint: opt.Endpoint, 72 | mountPermissions: opt.MountPermissions, 73 | workingMountDir: opt.WorkingMountDir, 74 | defaultOnDeletePolicy: opt.DefaultOnDeletePolicy, 75 | version: driverName, 76 | } 77 | 78 | driver.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{ 79 | csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME, 80 | csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, 81 | }) 82 | 83 | driver.AddNodeServiceCapabilities([]csi.NodeServiceCapability_RPC_Type{ 84 | csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER, 85 | csi.NodeServiceCapability_RPC_UNKNOWN, 86 | }) 87 | 88 | return driver 89 | } 90 | 91 | func (d *Driver) Run() { 92 | versionMeta, err := GetVersionYAML(d.name) 93 | if err != nil { 94 | klog.Fatalf("%v", err) 95 | } 96 | klog.V(2).Infof("\nDRIVER INFORMATION:\n-------------------\n%s\n\nStreaming logs below:", versionMeta) 97 | 98 | mounter := mount.New("") 99 | server := NewNonBlockingGRPCServer() 100 | server.Start(d.endpoint, 101 | NewIdentityServer(d), 102 | NewControllerServer(d, mounter), 103 | NewNodeServer(d, mounter), 104 | ) 105 | server.Wait() 106 | } 107 | 108 | func (d *Driver) AddControllerServiceCapabilities(cl []csi.ControllerServiceCapability_RPC_Type) { 109 | var csc []*csi.ControllerServiceCapability 110 | for _, c := range cl { 111 | csc = append(csc, NewControllerServiceCapability(c)) 112 | } 113 | d.cscap = csc 114 | } 115 | 116 | func (d *Driver) AddNodeServiceCapabilities(nl []csi.NodeServiceCapability_RPC_Type) { 117 | var nsc []*csi.NodeServiceCapability 118 | for _, n := range nl { 119 | nsc = append(nsc, NewNodeServiceCapability(n)) 120 | } 121 | d.nscap = nsc 122 | } 123 | -------------------------------------------------------------------------------- /deploy/csi-webdav-controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: csi-webdav-controller 6 | namespace: kube-system 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: csi-webdav-controller 12 | template: 13 | metadata: 14 | labels: 15 | app: csi-webdav-controller 16 | spec: 17 | hostNetwork: true # controller also needs to mount webdav to create dir 18 | dnsPolicy: ClusterFirstWithHostNet # available values: Default, ClusterFirstWithHostNet, ClusterFirst 19 | serviceAccountName: webdav-csi-sa 20 | nodeSelector: 21 | kubernetes.io/os: linux # add "kubernetes.io/role: master" to run controller on master node 22 | priorityClassName: system-cluster-critical 23 | securityContext: 24 | seccompProfile: 25 | type: RuntimeDefault 26 | tolerations: 27 | - key: "node-role.kubernetes.io/master" 28 | operator: "Exists" 29 | effect: "NoSchedule" 30 | - key: "node-role.kubernetes.io/controlplane" 31 | operator: "Exists" 32 | effect: "NoSchedule" 33 | - key: "node-role.kubernetes.io/control-plane" 34 | operator: "Exists" 35 | effect: "NoSchedule" 36 | containers: 37 | - name: csi-provisioner 38 | image: registry.k8s.io/sig-storage/csi-provisioner:v5.2.0 39 | imagePullPolicy: IfNotPresent 40 | args: 41 | - "-v=2" 42 | - "--csi-address=$(ADDRESS)" 43 | - "--leader-election" 44 | - "--leader-election-namespace=kube-system" 45 | - "--extra-create-metadata=true" 46 | - "--timeout=1200s" 47 | env: 48 | - name: ADDRESS 49 | value: /csi/csi.sock 50 | volumeMounts: 51 | - mountPath: /csi 52 | name: socket-dir 53 | resources: 54 | limits: 55 | memory: 400Mi 56 | requests: 57 | cpu: 10m 58 | memory: 20Mi 59 | - name: liveness-probe 60 | image: registry.k8s.io/sig-storage/livenessprobe:v2.15.0 61 | imagePullPolicy: IfNotPresent 62 | args: 63 | - --csi-address=/csi/csi.sock 64 | - --probe-timeout=3s 65 | - --health-port=29652 66 | - --v=2 67 | volumeMounts: 68 | - name: socket-dir 69 | mountPath: /csi 70 | resources: 71 | limits: 72 | memory: 100Mi 73 | requests: 74 | cpu: 10m 75 | memory: 20Mi 76 | - name: webdav 77 | image: gitlab.desy.de:5555/cloud-public/csi-driver-webdav:v0.0.1 78 | imagePullPolicy: IfNotPresent 79 | securityContext: 80 | privileged: true 81 | capabilities: 82 | add: ["SYS_ADMIN"] 83 | allowPrivilegeEscalation: true 84 | args: 85 | - "-v=5" 86 | - "--nodeid=$(NODE_ID)" 87 | - "--endpoint=$(CSI_ENDPOINT)" 88 | env: 89 | - name: NODE_ID 90 | valueFrom: 91 | fieldRef: 92 | fieldPath: spec.nodeName 93 | - name: CSI_ENDPOINT 94 | value: unix:///csi/csi.sock 95 | ports: 96 | - containerPort: 29652 97 | name: healthz 98 | protocol: TCP 99 | livenessProbe: 100 | failureThreshold: 5 101 | httpGet: 102 | path: /healthz 103 | port: healthz 104 | initialDelaySeconds: 30 105 | timeoutSeconds: 10 106 | periodSeconds: 30 107 | volumeMounts: 108 | - name: pods-mount-dir 109 | mountPath: /var/lib/kubelet/pods 110 | mountPropagation: "Bidirectional" 111 | - mountPath: /csi 112 | name: socket-dir 113 | resources: 114 | limits: 115 | memory: 200Mi 116 | requests: 117 | cpu: 10m 118 | memory: 20Mi 119 | volumes: 120 | - name: pods-mount-dir 121 | hostPath: 122 | path: /var/lib/kubelet/pods 123 | type: Directory 124 | - name: socket-dir 125 | emptyDir: {} 126 | -------------------------------------------------------------------------------- /deploy/csi-webdav-node.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: DaemonSet 3 | apiVersion: apps/v1 4 | metadata: 5 | name: csi-webdav-node 6 | namespace: kube-system 7 | spec: 8 | updateStrategy: 9 | rollingUpdate: 10 | maxUnavailable: 1 11 | type: RollingUpdate 12 | selector: 13 | matchLabels: 14 | app: csi-webdav-node 15 | template: 16 | metadata: 17 | labels: 18 | app: csi-webdav-node 19 | spec: 20 | hostNetwork: true # original webdav connection would be broken without hostNetwork setting 21 | dnsPolicy: ClusterFirstWithHostNet # available values: Default, ClusterFirstWithHostNet, ClusterFirst 22 | serviceAccountName: webdav-csi-sa 23 | priorityClassName: system-node-critical 24 | securityContext: 25 | seccompProfile: 26 | type: RuntimeDefault 27 | nodeSelector: 28 | kubernetes.io/os: linux 29 | tolerations: 30 | - operator: "Exists" 31 | containers: 32 | - name: liveness-probe 33 | image: registry.k8s.io/sig-storage/livenessprobe:v2.15.0 34 | imagePullPolicy: IfNotPresent 35 | args: 36 | - --csi-address=/csi/csi.sock 37 | - --probe-timeout=3s 38 | - --health-port=29653 39 | - --v=2 40 | volumeMounts: 41 | - name: socket-dir 42 | mountPath: /csi 43 | resources: 44 | limits: 45 | memory: 100Mi 46 | requests: 47 | cpu: 10m 48 | memory: 20Mi 49 | - name: node-driver-registrar 50 | image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.13.0 51 | imagePullPolicy: IfNotPresent 52 | args: 53 | - --v=2 54 | - --csi-address=/csi/csi.sock 55 | - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH) 56 | livenessProbe: 57 | exec: 58 | command: 59 | - /csi-node-driver-registrar 60 | - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH) 61 | - --mode=kubelet-registration-probe 62 | initialDelaySeconds: 30 63 | timeoutSeconds: 15 64 | env: 65 | - name: DRIVER_REG_SOCK_PATH 66 | value: /var/lib/kubelet/plugins/csi-webdavplugin/csi.sock 67 | - name: KUBE_NODE_NAME 68 | valueFrom: 69 | fieldRef: 70 | fieldPath: spec.nodeName 71 | volumeMounts: 72 | - name: socket-dir 73 | mountPath: /csi 74 | - name: registration-dir 75 | mountPath: /registration 76 | resources: 77 | limits: 78 | memory: 100Mi 79 | requests: 80 | cpu: 10m 81 | memory: 20Mi 82 | - name: webdav 83 | securityContext: 84 | privileged: true 85 | capabilities: 86 | add: ["SYS_ADMIN"] 87 | allowPrivilegeEscalation: true 88 | image: gitlab.desy.de:5555/cloud-public/csi-driver-webdav:v0.0.1 89 | imagePullPolicy: IfNotPresent 90 | args: 91 | - "-v=5" 92 | - "--nodeid=$(NODE_ID)" 93 | - "--endpoint=$(CSI_ENDPOINT)" 94 | env: 95 | - name: NODE_ID 96 | valueFrom: 97 | fieldRef: 98 | fieldPath: spec.nodeName 99 | - name: CSI_ENDPOINT 100 | value: unix:///csi/csi.sock 101 | ports: 102 | - containerPort: 29653 103 | name: healthz 104 | protocol: TCP 105 | livenessProbe: 106 | failureThreshold: 5 107 | httpGet: 108 | path: /healthz 109 | port: healthz 110 | initialDelaySeconds: 30 111 | timeoutSeconds: 10 112 | periodSeconds: 30 113 | imagePullPolicy: "IfNotPresent" 114 | volumeMounts: 115 | - name: socket-dir 116 | mountPath: /csi 117 | - name: pods-mount-dir 118 | mountPath: /var/lib/kubelet/pods 119 | mountPropagation: "Bidirectional" 120 | resources: 121 | limits: 122 | memory: 300Mi 123 | requests: 124 | cpu: 10m 125 | memory: 20Mi 126 | volumes: 127 | - name: socket-dir 128 | hostPath: 129 | path: /var/lib/kubelet/plugins/csi-webdavplugin 130 | type: DirectoryOrCreate 131 | - name: pods-mount-dir 132 | hostPath: 133 | path: /var/lib/kubelet/pods 134 | type: Directory 135 | - hostPath: 136 | path: /var/lib/kubelet/plugins_registry 137 | type: Directory 138 | name: registration-dir 139 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/container-storage-interface/spec v1.11.0 h1:H/YKTOeUZwHtyPOr9raR+HgFmGluGCklulxDYxSdVNM= 2 | github.com/container-storage-interface/spec v1.11.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 5 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 6 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 7 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 8 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 9 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 10 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 12 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 14 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 15 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 21 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 22 | github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= 23 | github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= 24 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 25 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 26 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 27 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 28 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 29 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 30 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 31 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 32 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 33 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 34 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 35 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 36 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 37 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 38 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 39 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 40 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 41 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 42 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 43 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 44 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 45 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 46 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= 47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 48 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 49 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 50 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 51 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 53 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 54 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 55 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 56 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 57 | k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= 58 | k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 59 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 60 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 61 | -------------------------------------------------------------------------------- /pkg/webdav/mount/mount_helper_common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package mount 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "time" 23 | 24 | "k8s.io/klog/v2" 25 | ) 26 | 27 | // CleanupMountPoint unmounts the given path and deletes the remaining directory 28 | // if successful. If extensiveMountPointCheck is true IsNotMountPoint will be 29 | // called instead of IsLikelyNotMountPoint. IsNotMountPoint is more expensive 30 | // but properly handles bind mounts within the same fs. 31 | func CleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) error { 32 | pathExists, pathErr := PathExists(mountPath) 33 | if !pathExists && pathErr == nil { 34 | klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath) 35 | return nil 36 | } 37 | corruptedMnt := IsCorruptedMnt(pathErr) 38 | if pathErr != nil && !corruptedMnt { 39 | return fmt.Errorf("Error checking path: %v", pathErr) 40 | } 41 | return doCleanupMountPoint(mountPath, mounter, extensiveMountPointCheck, corruptedMnt) 42 | } 43 | 44 | func CleanupMountWithForce(mountPath string, mounter MounterForceUnmounter, extensiveMountPointCheck bool, umountTimeout time.Duration) error { 45 | pathExists, pathErr := PathExists(mountPath) 46 | if !pathExists && pathErr == nil { 47 | klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath) 48 | return nil 49 | } 50 | corruptedMnt := IsCorruptedMnt(pathErr) 51 | if pathErr != nil && !corruptedMnt { 52 | return fmt.Errorf("Error checking path: %v", pathErr) 53 | } 54 | 55 | if corruptedMnt || mounter.CanSafelySkipMountPointCheck() { 56 | klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)", 57 | mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck()) 58 | if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil { 59 | return err 60 | } 61 | return removePath(mountPath) 62 | } 63 | 64 | notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) 65 | // if mountPath is not a mount point, it's just been removed or there was an error 66 | if err != nil || notMnt { 67 | return err 68 | } 69 | 70 | klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath) 71 | if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil { 72 | return err 73 | } 74 | 75 | notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) 76 | // if mountPath is not a mount point, it's either just been removed or there was an error 77 | if notMnt { 78 | return err 79 | } 80 | // mountPath is still a mount point 81 | return fmt.Errorf("failed to cleanup mount point %v", mountPath) 82 | } 83 | 84 | // doCleanupMountPoint unmounts the given path and 85 | // deletes the remaining directory if successful. 86 | // if extensiveMountPointCheck is true 87 | // IsNotMountPoint will be called instead of IsLikelyNotMountPoint. 88 | // IsNotMountPoint is more expensive but properly handles bind mounts within the same fs. 89 | // if corruptedMnt is true, it means that the mountPath is a corrupted mountpoint, and the mount point check 90 | // will be skipped. The mount point check will also be skipped if the mounter supports it. 91 | func doCleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool, corruptedMnt bool) error { 92 | if corruptedMnt || mounter.CanSafelySkipMountPointCheck() { 93 | klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)", 94 | mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck()) 95 | if err := mounter.Unmount(mountPath); err != nil { 96 | return err 97 | } 98 | return removePath(mountPath) 99 | } 100 | 101 | notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) 102 | // if mountPath is not a mount point, it's just been removed or there was an error 103 | if err != nil || notMnt { 104 | return err 105 | } 106 | 107 | klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath) 108 | if err := mounter.Unmount(mountPath); err != nil { 109 | return err 110 | } 111 | 112 | notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) 113 | // if mountPath is not a mount point, it's either just been removed or there was an error 114 | if notMnt { 115 | return err 116 | } 117 | // mountPath is still a mount point 118 | return fmt.Errorf("failed to cleanup mount point %v", mountPath) 119 | } 120 | 121 | // removePathIfNotMountPoint verifies if given mountPath is a mount point if not it attempts 122 | // to remove the directory. Returns true and nil if directory was not a mount point and removed. 123 | func removePathIfNotMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) (bool, error) { 124 | var notMnt bool 125 | var err error 126 | 127 | if extensiveMountPointCheck { 128 | notMnt, err = IsNotMountPoint(mounter, mountPath) 129 | } else { 130 | notMnt, err = mounter.IsLikelyNotMountPoint(mountPath) 131 | } 132 | 133 | if err != nil { 134 | if os.IsNotExist(err) { 135 | klog.V(4).Infof("%q does not exist", mountPath) 136 | return true, nil 137 | } 138 | return notMnt, err 139 | } 140 | 141 | if notMnt { 142 | klog.Warningf("Warning: %q is not a mountpoint, deleting", mountPath) 143 | return notMnt, os.Remove(mountPath) 144 | } 145 | return notMnt, nil 146 | } 147 | 148 | // removePath attempts to remove the directory. Returns nil if the directory was removed or does not exist. 149 | func removePath(mountPath string) error { 150 | klog.V(4).Infof("Warning: deleting path %q", mountPath) 151 | err := os.Remove(mountPath) 152 | if os.IsNotExist(err) { 153 | klog.V(4).Infof("%q does not exist", mountPath) 154 | return nil 155 | } 156 | return err 157 | } 158 | -------------------------------------------------------------------------------- /pkg/webdav/node.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "strings" 23 | 24 | "github.com/container-storage-interface/spec/lib/go/csi" 25 | "google.golang.org/grpc/codes" 26 | "google.golang.org/grpc/status" 27 | "k8s.io/klog/v2" 28 | 29 | "github.com/sys-liqian/csi-driver-webdav/pkg/webdav/mount" 30 | ) 31 | 32 | type NodeServer struct { 33 | Driver *Driver 34 | mounter mount.Interface 35 | csi.UnimplementedNodeServer 36 | } 37 | 38 | func NewNodeServer(d *Driver, mounter mount.Interface) *NodeServer { 39 | return &NodeServer{ 40 | Driver: d, 41 | mounter: mounter, 42 | } 43 | } 44 | 45 | // NodePublishVolume implements csi.NodeServer. 46 | func (n *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { 47 | volCap := req.GetVolumeCapability() 48 | if volCap == nil { 49 | return nil, status.Error(codes.InvalidArgument, "Volume capability missing in request") 50 | } 51 | volumeID := req.GetVolumeId() 52 | if len(volumeID) == 0 { 53 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 54 | } 55 | targetPath := req.GetTargetPath() 56 | if len(targetPath) == 0 { 57 | return nil, status.Error(codes.InvalidArgument, "Target path not provided") 58 | } 59 | 60 | mountOptions := volCap.GetMount().GetMountFlags() 61 | if req.GetReadonly() { 62 | mountOptions = append(mountOptions, "ro") 63 | } 64 | 65 | address, subDir, err := ParseVolumeId(volumeID) 66 | if err != nil { 67 | // An invalid ID should be treated as doesn't exist 68 | klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err) 69 | return &csi.NodePublishVolumeResponse{}, nil 70 | } 71 | 72 | notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath) 73 | if err != nil { 74 | if os.IsNotExist(err) { 75 | if err := os.MkdirAll(targetPath, os.FileMode(n.Driver.mountPermissions)); err != nil { 76 | return nil, status.Error(codes.Internal, err.Error()) 77 | } 78 | notMnt = true 79 | } else { 80 | return nil, status.Error(codes.Internal, err.Error()) 81 | } 82 | } 83 | if !notMnt { 84 | return &csi.NodePublishVolumeResponse{}, nil 85 | } 86 | 87 | sourcePath := strings.Join([]string{address, subDir}, "/") 88 | stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]} 89 | klog.V(2).Infof("NodePublishVolume: volumeID(%v) source(%s) targetPath(%s) mountflags(%v)", volumeID, sourcePath, targetPath, mountOptions) 90 | err = n.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, mountOptions, nil, stdin) 91 | if err != nil { 92 | if os.IsPermission(err) { 93 | return nil, status.Error(codes.PermissionDenied, err.Error()) 94 | } 95 | if strings.Contains(err.Error(), "invalid argument") { 96 | return nil, status.Error(codes.InvalidArgument, err.Error()) 97 | } 98 | return nil, status.Error(codes.Internal, err.Error()) 99 | } 100 | 101 | return &csi.NodePublishVolumeResponse{}, nil 102 | } 103 | 104 | // NodeUnpublishVolume implements csi.NodeServer. 105 | func (n *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) { 106 | volumeID := req.GetVolumeId() 107 | if len(volumeID) == 0 { 108 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 109 | } 110 | targetPath := req.GetTargetPath() 111 | if len(targetPath) == 0 { 112 | return nil, status.Error(codes.InvalidArgument, "Target path missing in request") 113 | } 114 | 115 | notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath) 116 | if err != nil { 117 | if os.IsNotExist(err) { 118 | return nil, status.Error(codes.NotFound, "Targetpath not found") 119 | } 120 | return nil, status.Error(codes.Internal, err.Error()) 121 | } 122 | if notMnt { 123 | return &csi.NodeUnpublishVolumeResponse{}, nil 124 | } 125 | 126 | klog.V(2).Infof("NodeUnpublishVolume: unmounting volume %s on %s", volumeID, targetPath) 127 | err = n.mounter.Unmount(targetPath) 128 | if err != nil { 129 | return nil, status.Errorf(codes.Internal, "failed to unmount target %q: %v", targetPath, err) 130 | } 131 | 132 | klog.V(2).Infof("NodeUnpublishVolume: unmount volume %s on %s successfully", volumeID, targetPath) 133 | return &csi.NodeUnpublishVolumeResponse{}, nil 134 | } 135 | 136 | // NodeGetInfo implements csi.NodeServer. 137 | func (n *NodeServer) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) { 138 | return &csi.NodeGetInfoResponse{NodeId: n.Driver.nodeID}, nil 139 | } 140 | 141 | // NodeGetCapabilities implements csi.NodeServer. 142 | func (n *NodeServer) NodeGetCapabilities(context.Context, *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { 143 | return &csi.NodeGetCapabilitiesResponse{ 144 | Capabilities: n.Driver.nscap, 145 | }, nil 146 | } 147 | 148 | // NodeExpandVolume implements csi.NodeServer. 149 | func (*NodeServer) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) { 150 | return nil, status.Error(codes.Unimplemented, "") 151 | } 152 | 153 | // NodeGetVolumeStats implements csi.NodeServer. 154 | func (*NodeServer) NodeGetVolumeStats(context.Context, *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { 155 | return nil, status.Error(codes.Unimplemented, "") 156 | } 157 | 158 | // NodeStageVolume implements csi.NodeServer. 159 | func (*NodeServer) NodeStageVolume(context.Context, *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) { 160 | return nil, status.Error(codes.Unimplemented, "") 161 | } 162 | 163 | // NodeUnstageVolume implements csi.NodeServer. 164 | func (*NodeServer) NodeUnstageVolume(context.Context, *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) { 165 | return nil, status.Error(codes.Unimplemented, "") 166 | } 167 | -------------------------------------------------------------------------------- /pkg/webdav/mount/mount_helper_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2019 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package mount 21 | 22 | import ( 23 | "bytes" 24 | "errors" 25 | "fmt" 26 | "io/fs" 27 | "os" 28 | "strconv" 29 | "strings" 30 | "sync" 31 | "syscall" 32 | 33 | "golang.org/x/sys/unix" 34 | "k8s.io/klog/v2" 35 | utilio "k8s.io/utils/io" 36 | ) 37 | 38 | const ( 39 | // At least number of fields per line in /proc//mountinfo. 40 | expectedAtLeastNumFieldsPerMountInfo = 10 41 | // How many times to retry for a consistent read of /proc/mounts. 42 | maxListTries = 10 43 | ) 44 | 45 | // IsCorruptedMnt return true if err is about corrupted mount point 46 | func IsCorruptedMnt(err error) bool { 47 | if err == nil { 48 | return false 49 | } 50 | var underlyingError error 51 | switch pe := err.(type) { 52 | case nil: 53 | return false 54 | case *os.PathError: 55 | underlyingError = pe.Err 56 | case *os.LinkError: 57 | underlyingError = pe.Err 58 | case *os.SyscallError: 59 | underlyingError = pe.Err 60 | case syscall.Errno: 61 | underlyingError = err 62 | } 63 | 64 | return underlyingError == syscall.ENOTCONN || underlyingError == syscall.ESTALE || underlyingError == syscall.EIO || underlyingError == syscall.EACCES || underlyingError == syscall.EHOSTDOWN 65 | } 66 | 67 | // MountInfo represents a single line in /proc//mountinfo. 68 | type MountInfo struct { // nolint: golint 69 | // Unique ID for the mount (maybe reused after umount). 70 | ID int 71 | // The ID of the parent mount (or of self for the root of this mount namespace's mount tree). 72 | ParentID int 73 | // Major indicates one half of the device ID which identifies the device class 74 | // (parsed from `st_dev` for files on this filesystem). 75 | Major int 76 | // Minor indicates one half of the device ID which identifies a specific 77 | // instance of device (parsed from `st_dev` for files on this filesystem). 78 | Minor int 79 | // The pathname of the directory in the filesystem which forms the root of this mount. 80 | Root string 81 | // Mount source, filesystem-specific information. e.g. device, tmpfs name. 82 | Source string 83 | // Mount point, the pathname of the mount point. 84 | MountPoint string 85 | // Optional fieds, zero or more fields of the form "tag[:value]". 86 | OptionalFields []string 87 | // The filesystem type in the form "type[.subtype]". 88 | FsType string 89 | // Per-mount options. 90 | MountOptions []string 91 | // Per-superblock options. 92 | SuperOptions []string 93 | } 94 | 95 | // ParseMountInfo parses /proc/xxx/mountinfo. 96 | func ParseMountInfo(filename string) ([]MountInfo, error) { 97 | content, err := readMountInfo(filename) 98 | if err != nil { 99 | return []MountInfo{}, err 100 | } 101 | contentStr := string(content) 102 | infos := []MountInfo{} 103 | 104 | for _, line := range strings.Split(contentStr, "\n") { 105 | if line == "" { 106 | // the last split() item is empty string following the last \n 107 | continue 108 | } 109 | // See `man proc` for authoritative description of format of the file. 110 | fields := strings.Fields(line) 111 | if len(fields) < expectedAtLeastNumFieldsPerMountInfo { 112 | return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line) 113 | } 114 | id, err := strconv.Atoi(fields[0]) 115 | if err != nil { 116 | return nil, err 117 | } 118 | parentID, err := strconv.Atoi(fields[1]) 119 | if err != nil { 120 | return nil, err 121 | } 122 | mm := strings.Split(fields[2], ":") 123 | if len(mm) != 2 { 124 | return nil, fmt.Errorf("parsing '%s' failed: unexpected minor:major pair %s", line, mm) 125 | } 126 | major, err := strconv.Atoi(mm[0]) 127 | if err != nil { 128 | return nil, fmt.Errorf("parsing '%s' failed: unable to parse major device id, err:%v", mm[0], err) 129 | } 130 | minor, err := strconv.Atoi(mm[1]) 131 | if err != nil { 132 | return nil, fmt.Errorf("parsing '%s' failed: unable to parse minor device id, err:%v", mm[1], err) 133 | } 134 | 135 | info := MountInfo{ 136 | ID: id, 137 | ParentID: parentID, 138 | Major: major, 139 | Minor: minor, 140 | Root: fields[3], 141 | MountPoint: fields[4], 142 | MountOptions: splitMountOptions(fields[5]), 143 | } 144 | // All fields until "-" are "optional fields". 145 | i := 6 146 | for ; i < len(fields) && fields[i] != "-"; i++ { 147 | info.OptionalFields = append(info.OptionalFields, fields[i]) 148 | } 149 | // Parse the rest 3 fields. 150 | i++ 151 | if len(fields)-i < 3 { 152 | return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i) 153 | } 154 | info.FsType = fields[i] 155 | info.Source = fields[i+1] 156 | info.SuperOptions = splitMountOptions(fields[i+2]) 157 | infos = append(infos, info) 158 | } 159 | return infos, nil 160 | } 161 | 162 | // splitMountOptions parses comma-separated list of mount options into an array. 163 | // It respects double quotes - commas in them are not considered as the option separator. 164 | func splitMountOptions(s string) []string { 165 | inQuotes := false 166 | list := strings.FieldsFunc(s, func(r rune) bool { 167 | if r == '"' { 168 | inQuotes = !inQuotes 169 | } 170 | // Report a new field only when outside of double quotes. 171 | return r == ',' && !inQuotes 172 | }) 173 | return list 174 | } 175 | 176 | // isMountPointMatch returns true if the path in mp is the same as dir. 177 | // Handles case where mountpoint dir has been renamed due to stale NFS mount. 178 | func isMountPointMatch(mp MountPoint, dir string) bool { 179 | return strings.TrimSuffix(mp.Path, "\\040(deleted)") == dir 180 | } 181 | 182 | // PathExists returns true if the specified path exists. 183 | // TODO: clean this up to use pkg/util/file/FileExists 184 | func PathExists(path string) (bool, error) { 185 | _, err := os.Stat(path) 186 | if err == nil { 187 | return true, nil 188 | } else if errors.Is(err, fs.ErrNotExist) { 189 | err = syscall.Access(path, syscall.F_OK) 190 | if err == nil { 191 | // The access syscall says the file exists, the stat syscall says it 192 | // doesn't. This was observed on CIFS when the path was removed at 193 | // the server somehow. POSIX calls this a stale file handle, let's fake 194 | // that error and treat the path as existing but corrupted. 195 | klog.Warningf("Potential stale file handle detected: %s", path) 196 | return true, syscall.ESTALE 197 | } 198 | return false, nil 199 | } else if IsCorruptedMnt(err) { 200 | return true, err 201 | } 202 | return false, err 203 | } 204 | 205 | // These variables are used solely by kernelHasMountinfoBug. 206 | var ( 207 | hasMountinfoBug bool 208 | checkMountinfoBugOnce sync.Once 209 | ) 210 | 211 | // kernelHasMountinfoBug checks if the kernel bug that can lead to incomplete 212 | // mountinfo being read is fixed. It does so by checking the kernel version. 213 | // 214 | // The bug was fixed by the kernel commit 9f6c61f96f2d97 (since Linux 5.8). 215 | // Alas, there is no better way to check if the bug is fixed other than to 216 | // rely on the kernel version returned by uname. 217 | func kernelHasMountinfoBug() bool { 218 | checkMountinfoBugOnce.Do(func() { 219 | // Assume old kernel. 220 | hasMountinfoBug = true 221 | 222 | uname := unix.Utsname{} 223 | err := unix.Uname(&uname) 224 | if err != nil { 225 | return 226 | } 227 | 228 | end := bytes.IndexByte(uname.Release[:], 0) 229 | v := bytes.SplitN(uname.Release[:end], []byte{'.'}, 3) 230 | if len(v) != 3 { 231 | return 232 | } 233 | major, _ := strconv.Atoi(string(v[0])) 234 | minor, _ := strconv.Atoi(string(v[1])) 235 | 236 | if major > 5 || (major == 5 && minor >= 8) { 237 | hasMountinfoBug = false 238 | } 239 | }) 240 | 241 | return hasMountinfoBug 242 | } 243 | 244 | func readMountInfo(path string) ([]byte, error) { 245 | if kernelHasMountinfoBug() { 246 | return utilio.ConsistentRead(path, maxListTries) 247 | } 248 | 249 | return os.ReadFile(path) 250 | } 251 | -------------------------------------------------------------------------------- /pkg/webdav/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package webdav 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | "strconv" 25 | "strings" 26 | 27 | "github.com/container-storage-interface/spec/lib/go/csi" 28 | "google.golang.org/grpc/codes" 29 | "google.golang.org/grpc/status" 30 | "k8s.io/klog/v2" 31 | 32 | "github.com/sys-liqian/csi-driver-webdav/pkg/webdav/mount" 33 | ) 34 | 35 | type ControllerServer struct { 36 | *Driver 37 | mounter mount.Interface 38 | csi.UnimplementedControllerServer 39 | } 40 | 41 | func NewControllerServer(d *Driver, mounter mount.Interface) *ControllerServer { 42 | return &ControllerServer{ 43 | Driver: d, 44 | mounter: mounter, 45 | } 46 | } 47 | 48 | // CreateVolume implements csi.ControllerServer. 49 | func (c *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) { 50 | name := req.GetName() 51 | if len(name) == 0 { 52 | return nil, status.Error(codes.InvalidArgument, "CreateVolume name must be provided") 53 | } 54 | if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil { 55 | return nil, status.Error(codes.InvalidArgument, err.Error()) 56 | } 57 | 58 | mountPermissions := c.Driver.mountPermissions 59 | parameters := req.GetParameters() 60 | if parameters == nil { 61 | parameters = make(map[string]string) 62 | } 63 | for k, v := range parameters { 64 | switch strings.ToLower(k) { 65 | case webdavSharePath, pvcNameKey, pvcNamespaceKey, pvNameKey: 66 | case mountPermissionsField: 67 | if v != "" { 68 | var err error 69 | if mountPermissions, err = strconv.ParseUint(v, 8, 32); err != nil { 70 | return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid mountPermissions %s in storage class", v)) 71 | } 72 | } 73 | default: 74 | return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid parameter %q in storage class", k)) 75 | } 76 | } 77 | 78 | targetPath := c.workingMountDir 79 | sourcePath := req.Parameters[webdavSharePath] 80 | notMnt, err := c.mounter.IsLikelyNotMountPoint(targetPath) 81 | if err != nil { 82 | if os.IsNotExist(err) { 83 | if err := os.MkdirAll(targetPath, 0750); err != nil { 84 | return nil, status.Error(codes.Internal, err.Error()) 85 | } 86 | notMnt = true 87 | } else { 88 | return nil, status.Error(codes.Internal, err.Error()) 89 | } 90 | } 91 | if !notMnt { 92 | return nil, status.Errorf(codes.Internal, fmt.Sprintf("target path %s is alredy mounted", targetPath)) 93 | } 94 | 95 | stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]} 96 | if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil { 97 | return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error())) 98 | } 99 | 100 | internalVolumePath := filepath.Join(targetPath, req.Name) 101 | if err = os.Mkdir(internalVolumePath, 0777); err != nil && !os.IsExist(err) { 102 | return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error()) 103 | } 104 | 105 | defer func() { 106 | if err = c.mounter.Unmount(targetPath); err != nil { 107 | klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error()) 108 | } 109 | }() 110 | 111 | if mountPermissions > 0 { 112 | // Reset directory permissions because of umask problems 113 | if err = os.Chmod(internalVolumePath, os.FileMode(mountPermissions)); err != nil { 114 | klog.Warningf("failed to chmod subdirectory: %v", err.Error()) 115 | } 116 | } 117 | 118 | return &csi.CreateVolumeResponse{ 119 | Volume: &csi.Volume{ 120 | VolumeId: MakeVolumeId(sourcePath, req.Name), 121 | CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size 122 | VolumeContext: nil, 123 | ContentSource: req.GetVolumeContentSource(), 124 | }, 125 | }, nil 126 | 127 | } 128 | 129 | // DeleteVolume implements csi.ControllerServer. 130 | func (c *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { 131 | volumeID := req.GetVolumeId() 132 | if volumeID == "" { 133 | return nil, status.Error(codes.InvalidArgument, "volume id is empty") 134 | } 135 | sourcePath, subDir, err := ParseVolumeId(volumeID) 136 | if err != nil { 137 | // An invalid ID should be treated as doesn't exist 138 | klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err) 139 | return &csi.DeleteVolumeResponse{}, nil 140 | } 141 | 142 | stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]} 143 | targetPath := c.workingMountDir 144 | if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil { 145 | return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error())) 146 | } 147 | 148 | defer func() { 149 | if err = c.mounter.Unmount(targetPath); err != nil { 150 | klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error()) 151 | } 152 | }() 153 | 154 | internalVolumePath := filepath.Join(targetPath, subDir) 155 | klog.V(2).Infof("Removing subdirectory at %v", internalVolumePath) 156 | if err = os.RemoveAll(internalVolumePath); err != nil { 157 | return nil, status.Errorf(codes.Internal, "failed to delete subdirectory: %v", err.Error()) 158 | } 159 | 160 | return &csi.DeleteVolumeResponse{}, nil 161 | } 162 | 163 | // ValidateVolumeCapabilities implements csi.ControllerServer. 164 | func (c *ControllerServer) ValidateVolumeCapabilities(_ context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) { 165 | if len(req.GetVolumeId()) == 0 { 166 | return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request") 167 | } 168 | if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil { 169 | return nil, status.Error(codes.InvalidArgument, err.Error()) 170 | } 171 | 172 | return &csi.ValidateVolumeCapabilitiesResponse{ 173 | Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{ 174 | VolumeCapabilities: req.GetVolumeCapabilities(), 175 | }, 176 | Message: "", 177 | }, nil 178 | } 179 | 180 | // ControllerGetCapabilities implements csi.ControllerServer. 181 | func (c *ControllerServer) ControllerGetCapabilities(context.Context, *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) { 182 | return &csi.ControllerGetCapabilitiesResponse{ 183 | Capabilities: c.Driver.cscap, 184 | }, nil 185 | } 186 | 187 | // ControllerExpandVolume implements csi.ControllerServer. 188 | func (*ControllerServer) ControllerExpandVolume(context.Context, *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) { 189 | return nil, status.Error(codes.Unimplemented, "") 190 | } 191 | 192 | // ControllerGetVolume implements csi.ControllerServer. 193 | func (*ControllerServer) ControllerGetVolume(context.Context, *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) { 194 | return nil, status.Error(codes.Unimplemented, "") 195 | } 196 | 197 | // ControllerModifyVolume implements csi.ControllerServer. 198 | func (*ControllerServer) ControllerModifyVolume(context.Context, *csi.ControllerModifyVolumeRequest) (*csi.ControllerModifyVolumeResponse, error) { 199 | return nil, status.Error(codes.Unimplemented, "") 200 | } 201 | 202 | // ControllerPublishVolume implements csi.ControllerServer. 203 | func (*ControllerServer) ControllerPublishVolume(context.Context, *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { 204 | return nil, status.Error(codes.Unimplemented, "") 205 | } 206 | 207 | // ControllerUnpublishVolume implements csi.ControllerServer. 208 | func (*ControllerServer) ControllerUnpublishVolume(context.Context, *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) { 209 | return nil, status.Error(codes.Unimplemented, "") 210 | } 211 | 212 | // CreateSnapshot implements csi.ControllerServer. 213 | func (*ControllerServer) CreateSnapshot(context.Context, *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) { 214 | return nil, status.Error(codes.Unimplemented, "") 215 | } 216 | 217 | // DeleteSnapshot implements csi.ControllerServer. 218 | func (*ControllerServer) DeleteSnapshot(context.Context, *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) { 219 | return nil, status.Error(codes.Unimplemented, "") 220 | } 221 | 222 | // GetCapacity implements csi.ControllerServer. 223 | func (*ControllerServer) GetCapacity(context.Context, *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) { 224 | return nil, status.Error(codes.Unimplemented, "") 225 | } 226 | 227 | // ListSnapshots implements csi.ControllerServer. 228 | func (*ControllerServer) ListSnapshots(context.Context, *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) { 229 | return nil, status.Error(codes.Unimplemented, "") 230 | } 231 | 232 | // ListVolumes implements csi.ControllerServer. 233 | func (*ControllerServer) ListVolumes(context.Context, *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) { 234 | return nil, status.Error(codes.Unimplemented, "") 235 | } 236 | 237 | // isValidVolumeCapabilities validates the given VolumeCapability array is valid 238 | func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error { 239 | if len(volCaps) == 0 { 240 | return fmt.Errorf("volume capabilities missing in request") 241 | } 242 | for _, c := range volCaps { 243 | if c.GetBlock() != nil { 244 | return fmt.Errorf("block volume capability not supported") 245 | } 246 | } 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/webdav/mount/mount.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have 18 | // an alternate platform, we will need to abstract further. 19 | 20 | package mount 21 | 22 | import ( 23 | "fmt" 24 | "path/filepath" 25 | "strings" 26 | "time" 27 | 28 | utilexec "k8s.io/utils/exec" 29 | ) 30 | 31 | const ( 32 | // Default mount command if mounter path is not specified. 33 | defaultMountCommand = "mount" 34 | // Log message where sensitive mount options were removed 35 | sensitiveOptionsRemoved = "" 36 | ) 37 | 38 | // Interface defines the set of methods to allow for mount operations on a system. 39 | type Interface interface { 40 | // Mount mounts source to target as fstype with given options. 41 | // options MUST not contain sensitive material (like passwords). 42 | Mount(source string, target string, fstype string, options []string) error 43 | // MountSensitive is the same as Mount() but this method allows 44 | // sensitiveOptions to be passed in a separate parameter from the normal 45 | // mount options and ensures the sensitiveOptions are never logged. This 46 | // method should be used by callers that pass sensitive material (like 47 | // passwords) as mount options. 48 | MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error 49 | // MountSensitiveWithStdin 50 | MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error 51 | // MountSensitiveWithoutSystemd is the same as MountSensitive() but this method disable using systemd mount. 52 | MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error 53 | // MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd() with additional mount flags 54 | MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error 55 | // Unmount unmounts given target. 56 | Unmount(target string) error 57 | // List returns a list of all mounted filesystems. This can be large. 58 | // On some platforms, reading mounts directly from the OS is not guaranteed 59 | // consistent (i.e. it could change between chunked reads). This is guaranteed 60 | // to be consistent. 61 | List() ([]MountPoint, error) 62 | // IsLikelyNotMountPoint uses heuristics to determine if a directory 63 | // is not a mountpoint. 64 | // It should return ErrNotExist when the directory does not exist. 65 | // IsLikelyNotMountPoint does NOT properly detect all mountpoint types 66 | // most notably linux bind mounts and symbolic link. For callers that do not 67 | // care about such situations, this is a faster alternative to calling List() 68 | // and scanning that output. 69 | IsLikelyNotMountPoint(file string) (bool, error) 70 | // CanSafelySkipMountPointCheck indicates whether this mounter returns errors on 71 | // operations for targets that are not mount points. If this returns true, no such 72 | // errors will be returned. 73 | CanSafelySkipMountPointCheck() bool 74 | // IsMountPoint determines if a directory is a mountpoint. 75 | // It should return ErrNotExist when the directory does not exist. 76 | // IsMountPoint is more expensive than IsLikelyNotMountPoint. 77 | // IsMountPoint detects bind mounts in linux. 78 | // IsMountPoint may enumerate all the mountpoints using List() and 79 | // the list of mountpoints may be large, then it uses 80 | // isMountPointMatch to evaluate whether the directory is a mountpoint. 81 | IsMountPoint(file string) (bool, error) 82 | // GetMountRefs finds all mount references to pathname, returning a slice of 83 | // paths. Pathname can be a mountpoint path or a normal directory 84 | // (for bind mount). On Linux, pathname is excluded from the slice. 85 | // For example, if /dev/sdc was mounted at /path/a and /path/b, 86 | // GetMountRefs("/path/a") would return ["/path/b"] 87 | // GetMountRefs("/path/b") would return ["/path/a"] 88 | // On Windows there is no way to query all mount points; as long as pathname is 89 | // a valid mount, it will be returned. 90 | GetMountRefs(pathname string) ([]string, error) 91 | } 92 | 93 | // Compile-time check to ensure all Mounter implementations satisfy 94 | // the mount interface. 95 | var _ Interface = &Mounter{} 96 | 97 | type MounterForceUnmounter interface { 98 | Interface 99 | // UnmountWithForce unmounts given target but will retry unmounting with force option 100 | // after given timeout. 101 | UnmountWithForce(target string, umountTimeout time.Duration) error 102 | } 103 | 104 | // MountPoint represents a single line in /proc/mounts or /etc/fstab. 105 | type MountPoint struct { // nolint: golint 106 | Device string 107 | Path string 108 | Type string 109 | Opts []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged). 110 | Freq int 111 | Pass int 112 | } 113 | 114 | type MountErrorType string // nolint: golint 115 | 116 | const ( 117 | FilesystemMismatch MountErrorType = "FilesystemMismatch" 118 | HasFilesystemErrors MountErrorType = "HasFilesystemErrors" 119 | UnformattedReadOnly MountErrorType = "UnformattedReadOnly" 120 | FormatFailed MountErrorType = "FormatFailed" 121 | GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed" 122 | UnknownMountError MountErrorType = "UnknownMountError" 123 | ) 124 | 125 | type MountError struct { // nolint: golint 126 | Type MountErrorType 127 | Message string 128 | } 129 | 130 | func (mountError MountError) String() string { 131 | return mountError.Message 132 | } 133 | 134 | func (mountError MountError) Error() string { 135 | return mountError.Message 136 | } 137 | 138 | func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error { 139 | mountError := MountError{ 140 | Type: mountErrorValue, 141 | Message: fmt.Sprintf(format, args...), 142 | } 143 | return mountError 144 | } 145 | 146 | // SafeFormatAndMount probes a device to see if it is formatted. 147 | // Namely it checks to see if a file system is present. If so it 148 | // mounts it otherwise the device is formatted first then mounted. 149 | type SafeFormatAndMount struct { 150 | Interface 151 | Exec utilexec.Interface 152 | 153 | formatSem chan any 154 | formatTimeout time.Duration 155 | } 156 | 157 | func NewSafeFormatAndMount(mounter Interface, exec utilexec.Interface, opts ...Option) *SafeFormatAndMount { 158 | res := &SafeFormatAndMount{ 159 | Interface: mounter, 160 | Exec: exec, 161 | } 162 | for _, opt := range opts { 163 | opt(res) 164 | } 165 | return res 166 | } 167 | 168 | type Option func(*SafeFormatAndMount) 169 | 170 | // WithMaxConcurrentFormat sets the maximum number of concurrent format 171 | // operations executed by the mounter. The timeout controls the maximum 172 | // duration of a format operation before its concurrency token is released. 173 | // Once a token is released, it can be acquired by another concurrent format 174 | // operation. The original operation is allowed to complete. 175 | // If n < 1, concurrency is set to unlimited. 176 | func WithMaxConcurrentFormat(n int, timeout time.Duration) Option { 177 | return func(mounter *SafeFormatAndMount) { 178 | if n > 0 { 179 | mounter.formatSem = make(chan any, n) 180 | mounter.formatTimeout = timeout 181 | } 182 | } 183 | } 184 | 185 | // FormatAndMount formats the given disk, if needed, and mounts it. 186 | // That is if the disk is not formatted and it is not being mounted as 187 | // read-only it will format it first then mount it. Otherwise, if the 188 | // disk is already formatted or it is being mounted as read-only, it 189 | // will be mounted without formatting. 190 | // options MUST not contain sensitive material (like passwords). 191 | func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error { 192 | return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) 193 | } 194 | 195 | // FormatAndMountSensitive is the same as FormatAndMount but this method allows 196 | // sensitiveOptions to be passed in a separate parameter from the normal mount 197 | // options and ensures the sensitiveOptions are never logged. This method should 198 | // be used by callers that pass sensitive material (like passwords) as mount 199 | // options. 200 | func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 201 | return mounter.FormatAndMountSensitiveWithFormatOptions(source, target, fstype, options, sensitiveOptions, nil /* formatOptions */) 202 | } 203 | 204 | // FormatAndMountSensitiveWithFormatOptions behaves exactly the same as 205 | // FormatAndMountSensitive, but allows for options to be passed when the disk 206 | // is formatted. These options are NOT validated in any way and should never 207 | // come directly from untrusted user input as that would be an injection risk. 208 | func (mounter *SafeFormatAndMount) FormatAndMountSensitiveWithFormatOptions(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { 209 | return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions, formatOptions) 210 | } 211 | 212 | // getMountRefsByDev finds all references to the device provided 213 | // by mountPath; returns a list of paths. 214 | // Note that mountPath should be path after the evaluation of any symblolic links. 215 | // 216 | //lint:ignore U1000 Ignore unused function temporarily for debugging 217 | func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) { 218 | mps, err := mounter.List() 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | // Finding the device mounted to mountPath. 224 | diskDev := "" 225 | for i := range mps { 226 | if mountPath == mps[i].Path { 227 | diskDev = mps[i].Device 228 | break 229 | } 230 | } 231 | 232 | // Find all references to the device. 233 | var refs []string 234 | for i := range mps { 235 | if mps[i].Device == diskDev || mps[i].Device == mountPath { 236 | if mps[i].Path != mountPath { 237 | refs = append(refs, mps[i].Path) 238 | } 239 | } 240 | } 241 | return refs, nil 242 | } 243 | 244 | // IsNotMountPoint determines if a directory is a mountpoint. 245 | // It should return ErrNotExist when the directory does not exist. 246 | // IsNotMountPoint is more expensive than IsLikelyNotMountPoint 247 | // and depends on IsMountPoint. 248 | // 249 | // If an error occurs, it returns true (assuming it is not a mountpoint) 250 | // when ErrNotExist is returned for callers similar to IsLikelyNotMountPoint. 251 | // 252 | // Deprecated: This function is kept to keep changes backward compatible with 253 | // previous library version. Callers should prefer mounter.IsMountPoint. 254 | func IsNotMountPoint(mounter Interface, file string) (bool, error) { 255 | isMnt, err := mounter.IsMountPoint(file) 256 | if err != nil { 257 | return true, err 258 | } 259 | return !isMnt, nil 260 | } 261 | 262 | // GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts 263 | // returns the device name, reference count, and error code. 264 | func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) { 265 | mps, err := mounter.List() 266 | if err != nil { 267 | return "", 0, err 268 | } 269 | 270 | // Find the device name. 271 | // FIXME if multiple devices mounted on the same mount path, only the first one is returned. 272 | device := "" 273 | // If mountPath is symlink, need get its target path. 274 | slTarget, err := filepath.EvalSymlinks(mountPath) 275 | if err != nil { 276 | slTarget = mountPath 277 | } 278 | for i := range mps { 279 | if mps[i].Path == slTarget { 280 | device = mps[i].Device 281 | break 282 | } 283 | } 284 | 285 | // Find all references to the device. 286 | refCount := 0 287 | for i := range mps { 288 | if mps[i].Device == device { 289 | refCount++ 290 | } 291 | } 292 | return device, refCount, nil 293 | } 294 | 295 | // MakeBindOpts detects whether a bind mount is being requested and makes the remount options to 296 | // use in case of bind mount, due to the fact that bind mount doesn't respect mount options. 297 | // The list equals: 298 | // 299 | // options - 'bind' + 'remount' (no duplicate) 300 | func MakeBindOpts(options []string) (bool, []string, []string) { 301 | bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */) 302 | return bind, bindOpts, bindRemountOpts 303 | } 304 | 305 | // MakeBindOptsSensitive is the same as MakeBindOpts but this method allows 306 | // sensitiveOptions to be passed in a separate parameter from the normal mount 307 | // options and ensures the sensitiveOptions are never logged. This method should 308 | // be used by callers that pass sensitive material (like passwords) as mount 309 | // options. 310 | func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) { 311 | // Because we have an FD opened on the subpath bind mount, the "bind" option 312 | // needs to be included, otherwise the mount target will error as busy if you 313 | // remount as readonly. 314 | // 315 | // As a consequence, all read only bind mounts will no longer change the underlying 316 | // volume mount to be read only. 317 | bindRemountOpts := []string{"bind", "remount"} 318 | bindRemountSensitiveOpts := []string{} 319 | bind := false 320 | bindOpts := []string{"bind"} 321 | 322 | // _netdev is a userspace mount option and does not automatically get added when 323 | // bind mount is created and hence we must carry it over. 324 | if checkForNetDev(options, sensitiveOptions) { 325 | bindOpts = append(bindOpts, "_netdev") 326 | } 327 | 328 | for _, option := range options { 329 | switch option { 330 | case "bind": 331 | bind = true 332 | case "remount": 333 | default: 334 | bindRemountOpts = append(bindRemountOpts, option) 335 | } 336 | } 337 | 338 | for _, sensitiveOption := range sensitiveOptions { 339 | switch sensitiveOption { 340 | case "bind": 341 | bind = true 342 | case "remount": 343 | default: 344 | bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption) 345 | } 346 | } 347 | 348 | return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts 349 | } 350 | 351 | func checkForNetDev(options []string, sensitiveOptions []string) bool { 352 | for _, option := range options { 353 | if option == "_netdev" { 354 | return true 355 | } 356 | } 357 | for _, sensitiveOption := range sensitiveOptions { 358 | if sensitiveOption == "_netdev" { 359 | return true 360 | } 361 | } 362 | return false 363 | } 364 | 365 | // PathWithinBase checks if give path is within given base directory. 366 | func PathWithinBase(fullPath, basePath string) bool { 367 | rel, err := filepath.Rel(basePath, fullPath) 368 | if err != nil { 369 | return false 370 | } 371 | if StartsWithBackstep(rel) { 372 | // Needed to escape the base path. 373 | return false 374 | } 375 | return true 376 | } 377 | 378 | // StartsWithBackstep checks if the given path starts with a backstep segment. 379 | func StartsWithBackstep(rel string) bool { 380 | // normalize to / and check for ../ 381 | return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../") 382 | } 383 | 384 | // sanitizedOptionsForLogging will return a comma separated string containing 385 | // options and sensitiveOptions. Each entry in sensitiveOptions will be 386 | // replaced with the string sensitiveOptionsRemoved 387 | // e.g. o1,o2,, 388 | func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string { 389 | separator := "" 390 | if len(options) > 0 && len(sensitiveOptions) > 0 { 391 | separator = "," 392 | } 393 | 394 | sensitiveOptionsStart := "" 395 | sensitiveOptionsEnd := "" 396 | if len(sensitiveOptions) > 0 { 397 | sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1) 398 | sensitiveOptionsEnd = sensitiveOptionsRemoved 399 | } 400 | 401 | return strings.Join(options, ",") + 402 | separator + 403 | sensitiveOptionsStart + 404 | sensitiveOptionsEnd 405 | } 406 | -------------------------------------------------------------------------------- /pkg/webdav/mount/mount_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | /* 5 | Copyright 2014 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package mount 21 | 22 | import ( 23 | "context" 24 | "errors" 25 | "fmt" 26 | "io" 27 | "io/fs" 28 | "os" 29 | "os/exec" 30 | "path/filepath" 31 | "strconv" 32 | "strings" 33 | "syscall" 34 | "time" 35 | 36 | "github.com/moby/sys/mountinfo" 37 | 38 | "k8s.io/klog/v2" 39 | utilexec "k8s.io/utils/exec" 40 | ) 41 | 42 | const ( 43 | // Number of fields per line in /proc/mounts as per the fstab man page. 44 | expectedNumFieldsPerLine = 6 45 | // Location of the mount file to use 46 | procMountsPath = "/proc/mounts" 47 | // Location of the mountinfo file 48 | procMountInfoPath = "/proc/self/mountinfo" 49 | // 'fsck' found errors and corrected them 50 | fsckErrorsCorrected = 1 51 | // 'fsck' found errors but exited without correcting them 52 | fsckErrorsUncorrected = 4 53 | // Error thrown by exec cmd.Run() when process spawned by cmd.Start() completes before cmd.Wait() is called (see - k/k issue #103753) 54 | errNoChildProcesses = "wait: no child processes" 55 | // Error returned by some `umount` implementations when the specified path is not a mount point 56 | errNotMounted = "not mounted" 57 | ) 58 | 59 | // Mounter provides the default implementation of mount.Interface 60 | // for the linux platform. This implementation assumes that the 61 | // kubelet is running in the host's root mount namespace. 62 | type Mounter struct { 63 | mounterPath string 64 | withSystemd *bool 65 | trySystemd bool 66 | withSafeNotMountedBehavior bool 67 | } 68 | 69 | var _ MounterForceUnmounter = &Mounter{} 70 | 71 | // New returns a mount.Interface for the current system. 72 | // It provides options to override the default mounter behavior. 73 | // mounterPath allows using an alternative to `/bin/mount` for mounting. 74 | func New(mounterPath string) Interface { 75 | return &Mounter{ 76 | mounterPath: mounterPath, 77 | trySystemd: true, 78 | withSafeNotMountedBehavior: detectSafeNotMountedBehavior(), 79 | } 80 | } 81 | 82 | // NewWithoutSystemd returns a Linux specific mount.Interface for the current 83 | // system. It provides options to override the default mounter behavior. 84 | // mounterPath allows using an alternative to `/bin/mount` for mounting. Any 85 | // detection for systemd functionality is disabled with this Mounter. 86 | func NewWithoutSystemd(mounterPath string) Interface { 87 | return &Mounter{ 88 | mounterPath: mounterPath, 89 | trySystemd: false, 90 | withSafeNotMountedBehavior: detectSafeNotMountedBehavior(), 91 | } 92 | } 93 | 94 | // hasSystemd validates that the withSystemd bool is set, if it is not, 95 | // detectSystemd will be called once for this Mounter instance. 96 | func (mounter *Mounter) hasSystemd() bool { 97 | if !mounter.trySystemd { 98 | mounter.withSystemd = &mounter.trySystemd 99 | } 100 | 101 | if mounter.withSystemd == nil { 102 | withSystemd := detectSystemd() 103 | mounter.withSystemd = &withSystemd 104 | } 105 | 106 | return *mounter.withSystemd 107 | } 108 | 109 | // Mount mounts source to target as fstype with given options. 'source' and 'fstype' must 110 | // be an empty string in case it's not required, e.g. for remount, or for auto filesystem 111 | // type, where kernel handles fstype for you. The mount 'options' is a list of options, 112 | // currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is 113 | // required, call Mount with an empty string list or nil. 114 | func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { 115 | return mounter.MountSensitive(source, target, fstype, options, nil) 116 | } 117 | 118 | // MountSensitive is the same as Mount() but this method allows 119 | // sensitiveOptions to be passed in a separate parameter from the normal 120 | // mount options and ensures the sensitiveOptions are never logged. This 121 | // method should be used by callers that pass sensitive material (like 122 | // passwords) as mount options. 123 | func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 124 | // Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty. 125 | // All Linux distros are expected to be shipped with a mount utility that a support bind mounts. 126 | mounterPath := "" 127 | bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions) 128 | if bind { 129 | err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil) 130 | if err != nil { 131 | return err 132 | } 133 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil) 134 | } 135 | // The list of filesystems that require containerized mounter on GCI image cluster 136 | fsTypesNeedMounter := map[string]struct{}{ 137 | "nfs": {}, 138 | "glusterfs": {}, 139 | "ceph": {}, 140 | "cifs": {}, 141 | } 142 | if _, ok := fsTypesNeedMounter[fstype]; ok { 143 | mounterPath = mounter.mounterPath 144 | } 145 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, nil) 146 | } 147 | 148 | func (mounter *Mounter) MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error { 149 | // Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty. 150 | // All Linux distros are expected to be shipped with a mount utility that a support bind mounts. 151 | mounterPath := "" 152 | bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions) 153 | if bind { 154 | err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin) 155 | if err != nil { 156 | return err 157 | } 158 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin) 159 | } 160 | // The list of filesystems that require containerized mounter on GCI image cluster 161 | fsTypesNeedMounter := map[string]struct{}{ 162 | "nfs": {}, 163 | "glusterfs": {}, 164 | "ceph": {}, 165 | "cifs": {}, 166 | } 167 | if _, ok := fsTypesNeedMounter[fstype]; ok { 168 | mounterPath = mounter.mounterPath 169 | } 170 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, stdin) 171 | } 172 | 173 | // MountSensitiveWithoutSystemd is the same as MountSensitive() but disable using systemd mount. 174 | func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 175 | return mounter.MountSensitiveWithoutSystemdWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */) 176 | } 177 | 178 | // MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd with additional mount flags. 179 | func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { 180 | mounterPath := "" 181 | bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions) 182 | if bind { 183 | err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, mountFlags, false, nil) 184 | if err != nil { 185 | return err 186 | } 187 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, mountFlags, false, nil) 188 | } 189 | // The list of filesystems that require containerized mounter on GCI image cluster 190 | fsTypesNeedMounter := map[string]struct{}{ 191 | "nfs": {}, 192 | "glusterfs": {}, 193 | "ceph": {}, 194 | "cifs": {}, 195 | } 196 | if _, ok := fsTypesNeedMounter[fstype]; ok { 197 | mounterPath = mounter.mounterPath 198 | } 199 | return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, mountFlags, false, nil) 200 | } 201 | 202 | // doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used. 203 | // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) 204 | // systemdMountRequired is an extension of option to decide whether uses systemd mount. 205 | 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 { 206 | mountArgs, mountArgsLogStr := MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, mountFlags) 207 | if len(mounterPath) > 0 { 208 | mountArgs = append([]string{mountCmd}, mountArgs...) 209 | mountArgsLogStr = mountCmd + " " + mountArgsLogStr 210 | mountCmd = mounterPath 211 | } 212 | 213 | if systemdMountRequired && mounter.hasSystemd() { 214 | // Try to run mount via systemd-run --scope. This will escape the 215 | // service where kubelet runs and any fuse daemons will be started in a 216 | // specific scope. kubelet service than can be restarted without killing 217 | // these fuse daemons. 218 | // 219 | // Complete command line (when mounterPath is not used): 220 | // systemd-run --description=... --scope -- mount -t 221 | // 222 | // Expected flow: 223 | // * systemd-run creates a transient scope (=~ cgroup) and executes its 224 | // argument (/bin/mount) there. 225 | // * mount does its job, forks a fuse daemon if necessary and finishes. 226 | // (systemd-run --scope finishes at this point, returning mount's exit 227 | // code and stdout/stderr - thats one of --scope benefits). 228 | // * systemd keeps the fuse daemon running in the scope (i.e. in its own 229 | // cgroup) until the fuse daemon dies (another --scope benefit). 230 | // Kubelet service can be restarted and the fuse daemon survives. 231 | // * When the fuse daemon dies (e.g. during unmount) systemd removes the 232 | // scope automatically. 233 | // 234 | // systemd-mount is not used because it's too new for older distros 235 | // (CentOS 7, Debian Jessie). 236 | mountCmd, mountArgs, mountArgsLogStr = AddSystemdScopeSensitive("systemd-run", target, mountCmd, mountArgs, mountArgsLogStr) 237 | // } else { 238 | // No systemd-run on the host (or we failed to check it), assume kubelet 239 | // does not run as a systemd service. 240 | // No code here, mountCmd and mountArgs are already populated. 241 | } 242 | 243 | // Logging with sensitive mount options removed. 244 | klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgsLogStr) 245 | command := exec.Command(mountCmd, mountArgs...) 246 | if stdin != nil { 247 | writer, err := command.StdinPipe() 248 | if err != nil { 249 | klog.Errorf("Create stdin pipe failed: %v\nMounting command: %s\nMounting arguments: %s\n", err, mountCmd, mountArgsLogStr) 250 | return fmt.Errorf("create stdin pip failed: %v\nMounting command: %s\nMounting arguments: %s", err, mountCmd, mountArgsLogStr) 251 | } 252 | for _, v := range stdin { 253 | io.WriteString(writer, v) 254 | io.WriteString(writer, "\n") 255 | } 256 | writer.Close() 257 | } 258 | 259 | output, err := command.CombinedOutput() 260 | if err != nil { 261 | if err.Error() == errNoChildProcesses { 262 | if command.ProcessState.Success() { 263 | // We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753). 264 | return nil 265 | } 266 | // Rewrite err with the actual exit error of the process. 267 | err = &exec.ExitError{ProcessState: command.ProcessState} 268 | } 269 | klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, mountArgsLogStr, string(output)) 270 | return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s", 271 | err, mountCmd, mountArgsLogStr, string(output)) 272 | } 273 | return err 274 | } 275 | 276 | // detectSystemd returns true if OS runs with systemd as init. When not sure 277 | // (permission errors, ...), it returns false. 278 | // There may be different ways how to detect systemd, this one makes sure that 279 | // systemd-runs (needed by Mount()) works. 280 | func detectSystemd() bool { 281 | if _, err := exec.LookPath("systemd-run"); err != nil { 282 | klog.V(2).Infof("Detected OS without systemd") 283 | return false 284 | } 285 | // Try to run systemd-run --scope /bin/true, that should be enough 286 | // to make sure that systemd is really running and not just installed, 287 | // which happens when running in a container with a systemd-based image 288 | // but with different pid 1. 289 | cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true") 290 | output, err := cmd.CombinedOutput() 291 | if err != nil { 292 | klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS") 293 | klog.V(4).Infof("systemd-run output: %s, failed with: %v", string(output), err) 294 | return false 295 | } 296 | klog.V(2).Infof("Detected OS with systemd") 297 | return true 298 | } 299 | 300 | // detectSafeNotMountedBehavior returns true if the umount implementation replies "not mounted" 301 | // when the specified path is not mounted. When not sure (permission errors, ...), it returns false. 302 | // When possible, we will trust umount's message and avoid doing our own mount point checks. 303 | // More info: https://github.com/util-linux/util-linux/blob/v2.2/mount/umount.c#L179 304 | func detectSafeNotMountedBehavior() bool { 305 | return detectSafeNotMountedBehaviorWithExec(utilexec.New()) 306 | } 307 | 308 | // detectSafeNotMountedBehaviorWithExec is for testing with FakeExec. 309 | func detectSafeNotMountedBehaviorWithExec(exec utilexec.Interface) bool { 310 | // create a temp dir and try to umount it 311 | path, err := os.MkdirTemp("", "kubelet-detect-safe-umount") 312 | if err != nil { 313 | klog.V(4).Infof("Cannot create temp dir to detect safe 'not mounted' behavior: %v", err) 314 | return false 315 | } 316 | defer os.RemoveAll(path) 317 | cmd := exec.Command("umount", path) 318 | output, err := cmd.CombinedOutput() 319 | if err != nil { 320 | if strings.Contains(string(output), errNotMounted) { 321 | klog.V(4).Infof("Detected umount with safe 'not mounted' behavior") 322 | return true 323 | } 324 | klog.V(4).Infof("'umount %s' failed with: %v, output: %s", path, err, string(output)) 325 | } 326 | klog.V(4).Infof("Detected umount with unsafe 'not mounted' behavior") 327 | return false 328 | } 329 | 330 | // MakeMountArgs makes the arguments to the mount(8) command. 331 | // options MUST not contain sensitive material (like passwords). 332 | func MakeMountArgs(source, target, fstype string, options []string) (mountArgs []string) { 333 | mountArgs, _ = MakeMountArgsSensitive(source, target, fstype, options, nil /* sensitiveOptions */) 334 | return mountArgs 335 | } 336 | 337 | // MakeMountArgsSensitive makes the arguments to the mount(8) command. 338 | // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) 339 | func MakeMountArgsSensitive(source, target, fstype string, options []string, sensitiveOptions []string) (mountArgs []string, mountArgsLogStr string) { 340 | return MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */) 341 | } 342 | 343 | // MakeMountArgsSensitiveWithMountFlags makes the arguments to the mount(8) command. 344 | // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) 345 | // mountFlags are additional mount flags that are not related with the fstype 346 | // and mount options 347 | func MakeMountArgsSensitiveWithMountFlags(source, target, fstype string, options []string, sensitiveOptions []string, mountFlags []string) (mountArgs []string, mountArgsLogStr string) { 348 | // Build mount command as follows: 349 | // mount [$mountFlags] [-t $fstype] [-o $options] [$source] $target 350 | mountArgs = []string{} 351 | mountArgsLogStr = "" 352 | 353 | mountArgs = append(mountArgs, mountFlags...) 354 | mountArgsLogStr += strings.Join(mountFlags, " ") 355 | 356 | if len(fstype) > 0 { 357 | mountArgs = append(mountArgs, "-t", fstype) 358 | mountArgsLogStr += strings.Join(mountArgs, " ") 359 | } 360 | if len(options) > 0 || len(sensitiveOptions) > 0 { 361 | combinedOptions := []string{} 362 | combinedOptions = append(combinedOptions, options...) 363 | combinedOptions = append(combinedOptions, sensitiveOptions...) 364 | mountArgs = append(mountArgs, "-o", strings.Join(combinedOptions, ",")) 365 | // exclude sensitiveOptions from log string 366 | mountArgsLogStr += " -o " + sanitizedOptionsForLogging(options, sensitiveOptions) 367 | } 368 | if len(source) > 0 { 369 | mountArgs = append(mountArgs, source) 370 | mountArgsLogStr += " " + source 371 | } 372 | mountArgs = append(mountArgs, target) 373 | mountArgsLogStr += " " + target 374 | 375 | return mountArgs, mountArgsLogStr 376 | } 377 | 378 | // AddSystemdScope adds "system-run --scope" to given command line 379 | // If args contains sensitive material, use AddSystemdScopeSensitive to construct 380 | // a safe to log string. 381 | func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) { 382 | descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) 383 | systemdRunArgs := []string{descriptionArg, "--scope", "--", command} 384 | return systemdRunPath, append(systemdRunArgs, args...) 385 | } 386 | 387 | // AddSystemdScopeSensitive adds "system-run --scope" to given command line 388 | // It also accepts takes a sanitized string containing mount arguments, mountArgsLogStr, 389 | // and returns the string appended to the systemd command for logging. 390 | func AddSystemdScopeSensitive(systemdRunPath, mountName, command string, args []string, mountArgsLogStr string) (string, []string, string) { 391 | descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) 392 | systemdRunArgs := []string{descriptionArg, "--scope", "--", command} 393 | return systemdRunPath, append(systemdRunArgs, args...), strings.Join(systemdRunArgs, " ") + " " + mountArgsLogStr 394 | } 395 | 396 | // Unmount unmounts the target. 397 | // If the mounter has safe "not mounted" behavior, no error will be returned when the target is not a mount point. 398 | func (mounter *Mounter) Unmount(target string) error { 399 | klog.V(4).Infof("Unmounting %s", target) 400 | command := exec.Command("umount", target) 401 | output, err := command.CombinedOutput() 402 | if err != nil { 403 | return checkUmountError(target, command, output, err, mounter.withSafeNotMountedBehavior) 404 | } 405 | return nil 406 | } 407 | 408 | // UnmountWithForce unmounts given target but will retry unmounting with force option 409 | // after given timeout. 410 | func (mounter *Mounter) UnmountWithForce(target string, umountTimeout time.Duration) error { 411 | err := tryUnmount(target, mounter.withSafeNotMountedBehavior, umountTimeout) 412 | if err != nil { 413 | if err == context.DeadlineExceeded { 414 | klog.V(2).Infof("Timed out waiting for unmount of %s, trying with -f", target) 415 | err = forceUmount(target, mounter.withSafeNotMountedBehavior) 416 | } 417 | return err 418 | } 419 | return nil 420 | } 421 | 422 | // List returns a list of all mounted filesystems. 423 | func (*Mounter) List() ([]MountPoint, error) { 424 | return ListProcMounts(procMountsPath) 425 | } 426 | 427 | // IsLikelyNotMountPoint determines if a directory is not a mountpoint. 428 | // It is fast but not necessarily ALWAYS correct. If the path is in fact 429 | // a bind mount from one part of a mount to another it will not be detected. 430 | // It also can not distinguish between mountpoints and symbolic links. 431 | // mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b") 432 | // will return true. When in fact /tmp/b is a mount point. If this situation 433 | // is of interest to you, don't use this function... 434 | func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { 435 | stat, err := os.Stat(file) 436 | if err != nil { 437 | return true, err 438 | } 439 | rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/"))) 440 | if err != nil { 441 | return true, err 442 | } 443 | // If the directory has a different device as parent, then it is a mountpoint. 444 | if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev { 445 | return false, nil 446 | } 447 | 448 | return true, nil 449 | } 450 | 451 | // CanSafelySkipMountPointCheck relies on the detected behavior of umount when given a target that is not a mount point. 452 | func (mounter *Mounter) CanSafelySkipMountPointCheck() bool { 453 | return mounter.withSafeNotMountedBehavior 454 | } 455 | 456 | // GetMountRefs finds all mount references to pathname, returns a 457 | // list of paths. Path could be a mountpoint or a normal 458 | // directory (for bind mount). 459 | func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { 460 | pathExists, pathErr := PathExists(pathname) 461 | if !pathExists { 462 | return []string{}, nil 463 | } else if IsCorruptedMnt(pathErr) { 464 | klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname) 465 | return []string{}, nil 466 | } else if pathErr != nil { 467 | return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr) 468 | } 469 | realpath, err := filepath.EvalSymlinks(pathname) 470 | if err != nil { 471 | return nil, err 472 | } 473 | return SearchMountPoints(realpath, procMountInfoPath) 474 | } 475 | 476 | // checkAndRepairFileSystem checks and repairs filesystems using command fsck. 477 | func (mounter *SafeFormatAndMount) checkAndRepairFilesystem(source string) error { 478 | klog.V(4).Infof("Checking for issues with fsck on disk: %s", source) 479 | args := []string{"-a", source} 480 | out, err := mounter.Exec.Command("fsck", args...).CombinedOutput() 481 | if err != nil { 482 | ee, isExitError := err.(utilexec.ExitError) 483 | switch { 484 | case err == utilexec.ErrExecutableNotFound: 485 | klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.") 486 | case isExitError && ee.ExitStatus() == fsckErrorsCorrected: 487 | klog.Infof("Device %s has errors which were corrected by fsck.", source) 488 | case isExitError && ee.ExitStatus() == fsckErrorsUncorrected: 489 | return NewMountError(HasFilesystemErrors, "'fsck' found errors on device %s but could not correct them: %s", source, string(out)) 490 | case isExitError && ee.ExitStatus() > fsckErrorsUncorrected: 491 | klog.Infof("`fsck` error %s", string(out)) 492 | default: 493 | klog.Warningf("fsck on device %s failed with error %v, output: %v", source, err, string(out)) 494 | } 495 | } 496 | return nil 497 | } 498 | 499 | // formatAndMount uses unix utils to format and mount the given disk 500 | func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { 501 | readOnly := false 502 | for _, option := range options { 503 | if option == "ro" { 504 | readOnly = true 505 | break 506 | } 507 | } 508 | if !readOnly { 509 | // Check sensitiveOptions for ro 510 | for _, option := range sensitiveOptions { 511 | if option == "ro" { 512 | readOnly = true 513 | break 514 | } 515 | } 516 | } 517 | 518 | options = append(options, "defaults") 519 | mountErrorValue := UnknownMountError 520 | 521 | // Check if the disk is already formatted 522 | existingFormat, err := mounter.GetDiskFormat(source) 523 | if err != nil { 524 | return NewMountError(GetDiskFormatFailed, "failed to get disk format of disk %s: %v", source, err) 525 | } 526 | 527 | // Use 'ext4' as the default 528 | if len(fstype) == 0 { 529 | fstype = "ext4" 530 | } 531 | 532 | if existingFormat == "" { 533 | // Do not attempt to format the disk if mounting as readonly, return an error to reflect this. 534 | if readOnly { 535 | return NewMountError(UnformattedReadOnly, "cannot mount unformatted disk %s as we are manipulating it in read-only mode", source) 536 | } 537 | 538 | // Disk is unformatted so format it. 539 | args := []string{source} 540 | if fstype == "ext4" || fstype == "ext3" { 541 | args = []string{ 542 | "-F", // Force flag 543 | "-m0", // Zero blocks reserved for super-user 544 | source, 545 | } 546 | } else if fstype == "xfs" { 547 | args = []string{ 548 | "-f", // force flag 549 | source, 550 | } 551 | } 552 | args = append(formatOptions, args...) 553 | 554 | klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args) 555 | 556 | output, err := mounter.format(fstype, args) 557 | if err != nil { 558 | // Do not log sensitiveOptions only options 559 | sensitiveOptionsLog := sanitizedOptionsForLogging(options, sensitiveOptions) 560 | 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)) 561 | klog.Error(detailedErr) 562 | return NewMountError(FormatFailed, detailedErr) 563 | } 564 | 565 | klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target) 566 | } else { 567 | if fstype != existingFormat { 568 | // Verify that the disk is formatted with filesystem type we are expecting 569 | mountErrorValue = FilesystemMismatch 570 | klog.Warningf("Configured to mount disk %s as %s but current format is %s, things might break", source, existingFormat, fstype) 571 | } 572 | 573 | if !readOnly { 574 | // Run check tools on the disk to fix repairable issues, only do this for formatted volumes requested as rw. 575 | err := mounter.checkAndRepairFilesystem(source) 576 | if err != nil { 577 | return err 578 | } 579 | } 580 | } 581 | 582 | // Mount the disk 583 | klog.V(4).Infof("Attempting to mount disk %s in %s format at %s", source, fstype, target) 584 | if err := mounter.MountSensitive(source, target, fstype, options, sensitiveOptions); err != nil { 585 | return NewMountError(mountErrorValue, err.Error()) 586 | } 587 | 588 | return nil 589 | } 590 | 591 | func (mounter *SafeFormatAndMount) format(fstype string, args []string) ([]byte, error) { 592 | if mounter.formatSem != nil { 593 | done := make(chan struct{}) 594 | defer close(done) 595 | 596 | mounter.formatSem <- struct{}{} 597 | 598 | go func() { 599 | defer func() { <-mounter.formatSem }() 600 | 601 | timeout := time.NewTimer(mounter.formatTimeout) 602 | defer timeout.Stop() 603 | 604 | select { 605 | case <-done: 606 | case <-timeout.C: 607 | } 608 | }() 609 | } 610 | 611 | return mounter.Exec.Command("mkfs."+fstype, args...).CombinedOutput() 612 | } 613 | 614 | func getDiskFormat(exec utilexec.Interface, disk string) (string, error) { 615 | args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk} 616 | klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args) 617 | dataOut, err := exec.Command("blkid", args...).CombinedOutput() 618 | output := string(dataOut) 619 | klog.V(4).Infof("Output: %q", output) 620 | 621 | if err != nil { 622 | if exit, ok := err.(utilexec.ExitError); ok { 623 | if exit.ExitStatus() == 2 { 624 | // Disk device is unformatted. 625 | // For `blkid`, if the specified token (TYPE/PTTYPE, etc) was 626 | // not found, or no (specified) devices could be identified, an 627 | // exit code of 2 is returned. 628 | return "", nil 629 | } 630 | } 631 | klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err) 632 | return "", err 633 | } 634 | 635 | var fstype, pttype string 636 | 637 | lines := strings.Split(output, "\n") 638 | for _, l := range lines { 639 | if len(l) <= 0 { 640 | // Ignore empty line. 641 | continue 642 | } 643 | cs := strings.Split(l, "=") 644 | if len(cs) != 2 { 645 | return "", fmt.Errorf("blkid returns invalid output: %s", output) 646 | } 647 | // TYPE is filesystem type, and PTTYPE is partition table type, according 648 | // to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/. 649 | if cs[0] == "TYPE" { 650 | fstype = cs[1] 651 | } else if cs[0] == "PTTYPE" { 652 | pttype = cs[1] 653 | } 654 | } 655 | 656 | if len(pttype) > 0 { 657 | klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype) 658 | // Returns a special non-empty string as filesystem type, then kubelet 659 | // will not format it. 660 | return "unknown data, probably partitions", nil 661 | } 662 | 663 | return fstype, nil 664 | } 665 | 666 | // GetDiskFormat uses 'blkid' to see if the given disk is unformatted 667 | func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) { 668 | return getDiskFormat(mounter.Exec, disk) 669 | } 670 | 671 | // ListProcMounts is shared with NsEnterMounter 672 | func ListProcMounts(mountFilePath string) ([]MountPoint, error) { 673 | content, err := readMountInfo(mountFilePath) 674 | if err != nil { 675 | return nil, err 676 | } 677 | return parseProcMounts(content) 678 | } 679 | 680 | func parseProcMounts(content []byte) ([]MountPoint, error) { 681 | out := []MountPoint{} 682 | lines := strings.Split(string(content), "\n") 683 | for _, line := range lines { 684 | if line == "" { 685 | // the last split() item is empty string following the last \n 686 | continue 687 | } 688 | fields := strings.Fields(line) 689 | if len(fields) != expectedNumFieldsPerLine { 690 | // Do not log line in case it contains sensitive Mount options 691 | return nil, fmt.Errorf("wrong number of fields (expected %d, got %d)", expectedNumFieldsPerLine, len(fields)) 692 | } 693 | 694 | mp := MountPoint{ 695 | Device: fields[0], 696 | Path: fields[1], 697 | Type: fields[2], 698 | Opts: strings.Split(fields[3], ","), 699 | } 700 | 701 | freq, err := strconv.Atoi(fields[4]) 702 | if err != nil { 703 | return nil, err 704 | } 705 | mp.Freq = freq 706 | 707 | pass, err := strconv.Atoi(fields[5]) 708 | if err != nil { 709 | return nil, err 710 | } 711 | mp.Pass = pass 712 | 713 | out = append(out, mp) 714 | } 715 | return out, nil 716 | } 717 | 718 | // SearchMountPoints finds all mount references to the source, returns a list of 719 | // mountpoints. 720 | // The source can be a mount point or a normal directory (bind mount). We 721 | // didn't support device because there is no use case by now. 722 | // Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, 723 | // it's possible to mount a non-root path of a filesystem, so we need to use 724 | // root path and major:minor to represent mount source uniquely. 725 | // This implementation is shared between Linux and NsEnterMounter 726 | func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) { 727 | mis, err := ParseMountInfo(mountInfoPath) 728 | if err != nil { 729 | return nil, err 730 | } 731 | 732 | mountID := 0 733 | rootPath := "" 734 | major := -1 735 | minor := -1 736 | 737 | // Finding the underlying root path and major:minor if possible. 738 | // We need search in backward order because it's possible for later mounts 739 | // to overlap earlier mounts. 740 | for i := len(mis) - 1; i >= 0; i-- { 741 | if hostSource == mis[i].MountPoint || PathWithinBase(hostSource, mis[i].MountPoint) { 742 | // If it's a mount point or path under a mount point. 743 | mountID = mis[i].ID 744 | rootPath = filepath.Join(mis[i].Root, strings.TrimPrefix(hostSource, mis[i].MountPoint)) 745 | major = mis[i].Major 746 | minor = mis[i].Minor 747 | break 748 | } 749 | } 750 | 751 | if rootPath == "" || major == -1 || minor == -1 { 752 | return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource) 753 | } 754 | 755 | var refs []string 756 | for i := range mis { 757 | if mis[i].ID == mountID { 758 | // Ignore mount entry for mount source itself. 759 | continue 760 | } 761 | if mis[i].Root == rootPath && mis[i].Major == major && mis[i].Minor == minor { 762 | refs = append(refs, mis[i].MountPoint) 763 | } 764 | } 765 | 766 | return refs, nil 767 | } 768 | 769 | // IsMountPoint determines if a file is a mountpoint. 770 | // It first detects bind & any other mountpoints using 771 | // MountedFast function. If the MountedFast function returns 772 | // sure as true and err as nil, then a mountpoint is detected 773 | // successfully. When an error is returned by MountedFast, the 774 | // following is true: 775 | // 1. All errors are returned with IsMountPoint as false 776 | // except os.IsPermission. 777 | // 2. When os.IsPermission is returned by MountedFast, List() 778 | // is called to confirm if the given file is a mountpoint are not. 779 | // 780 | // os.ErrNotExist should always be returned if a file does not exist 781 | // as callers have in past relied on this error and not fallback. 782 | // 783 | // When MountedFast returns sure as false and err as nil (eg: in 784 | // case of bindmounts on kernel version 5.10- ); mounter.List() 785 | // endpoint is called to enumerate all the mountpoints and check if 786 | // it is mountpoint match or not. 787 | func (mounter *Mounter) IsMountPoint(file string) (bool, error) { 788 | isMnt, sure, isMntErr := mountinfo.MountedFast(file) 789 | if sure && isMntErr == nil { 790 | return isMnt, nil 791 | } 792 | if isMntErr != nil { 793 | if errors.Is(isMntErr, fs.ErrNotExist) { 794 | return false, fs.ErrNotExist 795 | } 796 | // We were not allowed to do the simple stat() check, e.g. on NFS with 797 | // root_squash. Fall back to /proc/mounts check below when 798 | // fs.ErrPermission is returned. 799 | if !errors.Is(isMntErr, fs.ErrPermission) { 800 | return false, isMntErr 801 | } 802 | } 803 | // Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts. 804 | resolvedFile, err := filepath.EvalSymlinks(file) 805 | if err != nil { 806 | if errors.Is(err, fs.ErrNotExist) { 807 | return false, fs.ErrNotExist 808 | } 809 | return false, err 810 | } 811 | 812 | // check all mountpoints since MountedFast is not sure. 813 | // is not reliable for some mountpoint types. 814 | mountPoints, mountPointsErr := mounter.List() 815 | if mountPointsErr != nil { 816 | return false, mountPointsErr 817 | } 818 | for _, mp := range mountPoints { 819 | if isMountPointMatch(mp, resolvedFile) { 820 | return true, nil 821 | } 822 | } 823 | return false, nil 824 | } 825 | 826 | // tryUnmount calls plain "umount" and waits for unmountTimeout for it to finish. 827 | func tryUnmount(target string, withSafeNotMountedBehavior bool, unmountTimeout time.Duration) error { 828 | klog.V(4).Infof("Unmounting %s", target) 829 | ctx, cancel := context.WithTimeout(context.Background(), unmountTimeout) 830 | defer cancel() 831 | 832 | command := exec.CommandContext(ctx, "umount", target) 833 | output, err := command.CombinedOutput() 834 | 835 | // CombinedOutput() does not return DeadlineExceeded, make sure it's 836 | // propagated on timeout. 837 | if ctx.Err() != nil { 838 | return ctx.Err() 839 | } 840 | 841 | if err != nil { 842 | return checkUmountError(target, command, output, err, withSafeNotMountedBehavior) 843 | } 844 | return nil 845 | } 846 | 847 | func forceUmount(target string, withSafeNotMountedBehavior bool) error { 848 | command := exec.Command("umount", "-f", target) 849 | output, err := command.CombinedOutput() 850 | if err != nil { 851 | return checkUmountError(target, command, output, err, withSafeNotMountedBehavior) 852 | } 853 | return nil 854 | } 855 | 856 | // checkUmountError checks a result of umount command and determine a return value. 857 | func checkUmountError(target string, command *exec.Cmd, output []byte, err error, withSafeNotMountedBehavior bool) error { 858 | if err.Error() == errNoChildProcesses { 859 | if command.ProcessState.Success() { 860 | // We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753). 861 | return nil 862 | } 863 | // Rewrite err with the actual exit error of the process. 864 | err = &exec.ExitError{ProcessState: command.ProcessState} 865 | } 866 | if withSafeNotMountedBehavior && strings.Contains(string(output), errNotMounted) { 867 | klog.V(4).Infof("ignoring 'not mounted' error for %s", target) 868 | return nil 869 | } 870 | return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output)) 871 | } 872 | --------------------------------------------------------------------------------