Compare commits
10 Commits
9f64c24808
...
9edb044d35
Author | SHA1 | Date |
---|---|---|
Ruakij | 9edb044d35 | 4 months ago |
Ruakij | a3414c37dd | 4 months ago |
Ruakij | 2287670f63 | 4 months ago |
sys-liqian | 680a68204e | 8 months ago |
sys-liqian | e42800379a | 8 months ago |
sys-liqian | ff95639565 | 1 year ago |
sys-liqian | 7051bb12b2 | 1 year ago |
sys-liqian | 16e0a83b1a | 1 year ago |
sys-liqian | df80de8392 | 1 year ago |
sys-liqian | f8aa58a3be | 1 year ago |
@ -0,0 +1,72 @@
|
||||
# OSX leaves these everywhere on SMB shares
|
||||
._*
|
||||
|
||||
# OSX trash
|
||||
.DS_Store
|
||||
|
||||
# Eclipse files
|
||||
.classpath
|
||||
.project
|
||||
.settings/**
|
||||
|
||||
# Files generated by JetBrains IDEs, e.g. IntelliJ IDEA
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Vscode files
|
||||
.vscode
|
||||
|
||||
# This is where the result of the go build goes
|
||||
/output*/
|
||||
/_output*/
|
||||
/_output
|
||||
/bin
|
||||
|
||||
# Emacs save files
|
||||
*~
|
||||
\#*\#
|
||||
.\#*
|
||||
|
||||
# Vim-related files
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
|
||||
# cscope-related files
|
||||
cscope.*
|
||||
|
||||
# Go test binaries
|
||||
*.test
|
||||
|
||||
# JUnit test output from ginkgo e2e tests
|
||||
/junit*.xml
|
||||
|
||||
# Mercurial files
|
||||
**/.hg
|
||||
**/.hg*
|
||||
|
||||
# Vagrant
|
||||
.vagrant
|
||||
|
||||
.tags*
|
||||
|
||||
# Test artifacts produced by Jenkins jobs
|
||||
/_artifacts/
|
||||
|
||||
# Go dependencies installed on Jenkins
|
||||
/_gopath/
|
||||
|
||||
# direnv .envrc files
|
||||
.envrc
|
||||
|
||||
# This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored
|
||||
!\.drone\.sec
|
||||
|
||||
# Godeps or dep workspace
|
||||
/Godeps/_workspace
|
||||
|
||||
/bazel-*
|
||||
*.pyc
|
||||
profile.cov
|
@ -0,0 +1,34 @@
|
||||
# Copyright 2023.
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ---- Build ----
|
||||
FROM golang:1.23-alpine AS build
|
||||
WORKDIR /build
|
||||
# Copy sources
|
||||
ADD . .
|
||||
# Get dependencies
|
||||
RUN make go-build
|
||||
# Compile
|
||||
RUN CGO_ENABLED=0 go build -a -o webdavplugin ./cmd/webdav
|
||||
|
||||
|
||||
# ---- Release ----
|
||||
FROM alpine AS release
|
||||
# Install required packages
|
||||
RUN apk add --no-cache davfs2
|
||||
|
||||
# Copy build-target
|
||||
COPY --from=build /build/webdavplugin .
|
||||
|
||||
ENTRYPOINT ["/webdavplugin"]
|
@ -0,0 +1,41 @@
|
||||
# Webdav CSI driver for Kubernetes
|
||||
|
||||
### Overview
|
||||
|
||||
This is a repository for webdav csi driver, csi plugin name: `webdav.csi.io`. This driver supports dynamic provisioning of Persistent Volumes via Persistent Volume Claims by creating a new sub directory under webdav server.
|
||||
|
||||
### Deploy CSI
|
||||
#### With Helm
|
||||
```bash
|
||||
helm install -n webdav-csi-driver webdav-csi-driver helm/
|
||||
```
|
||||
|
||||
### Quick start with kind
|
||||
|
||||
#### Build plugin image
|
||||
```bash
|
||||
make docker-build
|
||||
```
|
||||
|
||||
#### Start kind cluster
|
||||
```bash
|
||||
kind create cluster --image kindest/node:v1.27.3
|
||||
```
|
||||
|
||||
### Load plugin image to kind cluster
|
||||
```bash
|
||||
kind load docker-image registry.k8s.io/sig-storage/csi-provisioner:v3.6.2
|
||||
kind load docker-image registry.k8s.io/sig-storage/livenessprobe:v2.11.0
|
||||
kind load docker-image registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.1
|
||||
kind load docker-image localhost:5000/webdavplugin:v0.0.1
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Tests
|
||||
```bash
|
||||
kubectl apply -f examples/csi-webdav-secret.yaml
|
||||
kubectl apply -f examples/csi-webdav-storageclass.yaml
|
||||
kubectl apply -f examples/csi-webdav-dynamic-pvc.yaml
|
||||
kubectl apply -f examples/csi-webdav-pod.yaml
|
||||
```
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI endpoint")
|
||||
nodeID = flag.String("nodeid", "", "node id")
|
||||
mountPermissions = flag.Uint64("mount-permissions", 0, "mounted folder permissions")
|
||||
driverName = flag.String("drivername", "", "name of the driver")
|
||||
workingMountDir = flag.String("working-mount-dir", "/tmp/csi-storage", "working directory for provisioner to mount davfs shares temporarily")
|
||||
defaultOnDeletePolicy = flag.String("default-ondelete-policy", "", "default policy for deleting subdirectory when deleting a volume")
|
||||
)
|
||||
|
||||
func main() {
|
||||
klog.InitFlags(nil)
|
||||
_ = flag.Set("logtostderr", "true")
|
||||
flag.Parse()
|
||||
if *nodeID == "" {
|
||||
klog.Warning("nodeid is empty")
|
||||
}
|
||||
|
||||
driverOptions := webdav.DriverOpt{
|
||||
Name: *driverName,
|
||||
NodeID: *nodeID,
|
||||
Endpoint: *endpoint,
|
||||
MountPermissions: *mountPermissions,
|
||||
WorkingMountDir: *workingMountDir,
|
||||
DefaultOnDeletePolicy: *defaultOnDeletePolicy,
|
||||
}
|
||||
d := webdav.NewDriver(&driverOptions)
|
||||
d.Run()
|
||||
os.Exit(0)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: pvc-webdav-dynamic
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storageClassName: webdav-sc
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
volumeMounts:
|
||||
- name: pvc-webdav-dynamic
|
||||
mountPath: /var/www/html
|
||||
volumes:
|
||||
- name: pvc-webdav-dynamic
|
||||
persistentVolumeClaim:
|
||||
claimName: pvc-webdav-dynamic
|
@ -0,0 +1,9 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: webdav-secrect
|
||||
type: Opaque
|
||||
data:
|
||||
username: YWRtaW4=
|
||||
password: YWRtaW4=
|
@ -0,0 +1,16 @@
|
||||
---
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: webdav-sc
|
||||
provisioner: webdav.csi.io
|
||||
parameters:
|
||||
# alist folder webdav address
|
||||
share: http://ip:port/dav/media
|
||||
csi.storage.k8s.io/provisioner-secret-name: "webdav-secrect"
|
||||
csi.storage.k8s.io/provisioner-secret-namespace: "default"
|
||||
csi.storage.k8s.io/node-publish-secret-name: "webdav-secrect"
|
||||
csi.storage.k8s.io/node-publish-secret-namespace: "default"
|
||||
reclaimPolicy: Delete
|
||||
volumeBindingMode: Immediate
|
||||
mountOptions:
|
@ -0,0 +1,24 @@
|
||||
module git.ruekov.eu/ruakij/webdav-csi-driver
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/container-storage-interface/spec v1.10.0
|
||||
github.com/moby/sys/mountinfo v0.7.2
|
||||
golang.org/x/sys v0.24.0
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/utils v0.0.0-20240821151609-f90d01438635
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
@ -0,0 +1,42 @@
|
||||
github.com/container-storage-interface/spec v1.10.0 h1:YkzWPV39x+ZMTa6Ax2czJLLwpryrQ+dPesB34mrRMXA=
|
||||
github.com/container-storage-interface/spec v1.10.0/go.mod h1:DtUvaQszPml1YJfIK7c00mlv6/g4wNMLanLgiUbKFRI=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/utils v0.0.0-20240821151609-f90d01438635 h1:2wThSvJoW/Ncn9TmQEYXRnevZXi2duqHWf5OX9S3zjI=
|
||||
k8s.io/utils v0.0.0-20240821151609-f90d01438635/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: webdav-csi-driver
|
||||
description: A Helm chart for deploying CSI WebDAV Storage Driver
|
||||
version: 0.0.1
|
@ -0,0 +1,124 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: webdav-csi-controller
|
||||
namespace: kube-system
|
||||
spec:
|
||||
replicas: {{ .Values.controller.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: webdav-csi-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: webdav-csi-controller
|
||||
spec:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
serviceAccountName: webdav-csi-sa
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
priorityClassName: system-cluster-critical
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/master"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node-role.kubernetes.io/controlplane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
containers:
|
||||
- name: csi-provisioner
|
||||
image: {{ .Values.csiProvisioner.image.name }}:{{ .Values.csiProvisioner.image.tag }}
|
||||
imagePullPolicy: {{ .Values.csiProvisioner.image.pullPolicy }}
|
||||
args:
|
||||
- "-v=2"
|
||||
- "--csi-address=$(ADDRESS)"
|
||||
- "--leader-election"
|
||||
- "--leader-election-namespace=kube-system"
|
||||
- "--extra-create-metadata=true"
|
||||
- "--timeout=1200s"
|
||||
env:
|
||||
- name: ADDRESS
|
||||
value: /csi/csi.sock
|
||||
volumeMounts:
|
||||
- mountPath: /csi
|
||||
name: socket-dir
|
||||
resources:
|
||||
limits:
|
||||
memory: {{ .Values.controller.resources.limits.memory }}
|
||||
requests:
|
||||
cpu: {{ .Values.controller.resources.requests.cpu }}
|
||||
memory: {{ .Values.controller.resources.requests.memory }}
|
||||
- name: liveness-probe
|
||||
image: {{ .Values.livenessProbe.image.name }}:{{ .Values.livenessProbe.image.tag }}
|
||||
imagePullPolicy: {{ .Values.livenessProbe.image.pullPolicy }}
|
||||
args:
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --probe-timeout=3s
|
||||
- --health-port=29652
|
||||
- --v=2
|
||||
volumeMounts:
|
||||
- name: socket-dir
|
||||
mountPath: /csi
|
||||
resources:
|
||||
limits:
|
||||
memory: 100Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
- name: webdav
|
||||
image: {{ .Values.controller.image.name }}:{{ .Values.controller.image.tag }}
|
||||
imagePullPolicy: {{ .Values.controller.image.pullPolicy }}
|
||||
securityContext:
|
||||
privileged: true
|
||||
capabilities:
|
||||
add: ["SYS_ADMIN"]
|
||||
allowPrivilegeEscalation: true
|
||||
args:
|
||||
- "-v=5"
|
||||
- "--nodeid=$(NODE_ID)"
|
||||
- "--endpoint=$(CSI_ENDPOINT)"
|
||||
env:
|
||||
- name: NODE_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: CSI_ENDPOINT
|
||||
value: unix:///csi/csi.sock
|
||||
ports:
|
||||
- containerPort: 29652
|
||||
name: healthz
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
failureThreshold: 5
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: healthz
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 10
|
||||
periodSeconds: 30
|
||||
volumeMounts:
|
||||
- name: pods-mount-dir
|
||||
mountPath: /var/lib/kubelet/pods
|
||||
mountPropagation: "Bidirectional"
|
||||
- mountPath: /csi
|
||||
name: socket-dir
|
||||
resources:
|
||||
limits:
|
||||
memory: {{ .Values.controller.resources.limits.memory }}
|
||||
requests:
|
||||
cpu: {{ .Values.controller.resources.requests.cpu }}
|
||||
memory: {{ .Values.controller.resources.requests.memory }}
|
||||
volumes:
|
||||
- name: pods-mount-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/pods
|
||||
type: Directory
|
||||
- name: socket-dir
|
||||
emptyDir: {}
|
@ -0,0 +1,13 @@
|
||||
{{- if .defaultStorageClass }}
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: {{ .defaultStorageClass.name }}
|
||||
provisioner: webdav.csi.io
|
||||
parameters:
|
||||
{{- toYaml .defaultStorageClass.parameters | nindent 2 }}
|
||||
reclaimPolicy: {{ .defaultStorageClass.reclaimPolicy }}
|
||||
volumeBindingMode: {{ .defaultStorageClass.volumeBindingMode }}
|
||||
mountOptions:
|
||||
{{- toYaml .defaultStorageClass.mountOptions | nindent 2 }}
|
||||
{{- end }}
|
@ -0,0 +1,8 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: CSIDriver
|
||||
metadata:
|
||||
name: webdav.csi.io
|
||||
spec:
|
||||
attachRequired: false
|
||||
volumeLifecycleModes:
|
||||
- Persistent
|
@ -0,0 +1,136 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: webdav-csi-node
|
||||
namespace: kube-system
|
||||
spec:
|
||||
updateStrategy:
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: webdav-csi-node
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: webdav-csi-node
|
||||
spec:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
serviceAccountName: webdav-csi-sa
|
||||
priorityClassName: system-node-critical
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
tolerations:
|
||||
- operator: "Exists"
|
||||
containers:
|
||||
- name: liveness-probe
|
||||
image: {{ .Values.livenessProbe.image.name }}:{{ .Values.livenessProbe.image.tag }}
|
||||
imagePullPolicy: {{ .Values.livenessProbe.image.pullPolicy }}
|
||||
args:
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --probe-timeout=3s
|
||||
- --health-port=29653
|
||||
- --v=2
|
||||
volumeMounts:
|
||||
- name: socket-dir
|
||||
mountPath: /csi
|
||||
resources:
|
||||
limits:
|
||||
memory: 100Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
- name: node-driver-registrar
|
||||
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.9.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --v=2
|
||||
- --csi-address=/csi/csi.sock
|
||||
- --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /csi-node-driver-registrar
|
||||
- --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
|
||||
- --mode=kubelet-registration-probe
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 15
|
||||
env:
|
||||
- name: DRIVER_REG_SOCK_PATH
|
||||
value: /var/lib/kubelet/plugins/webdav-csiplugin/csi.sock
|
||||
- name: KUBE_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
volumeMounts:
|
||||
- name: socket-dir
|
||||
mountPath: /csi
|
||||
- name: registration-dir
|
||||
mountPath: /registration
|
||||
resources:
|
||||
limits:
|
||||
memory: 100Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
- name: webdav
|
||||
securityContext:
|
||||
privileged: true
|
||||
capabilities:
|
||||
add: ["SYS_ADMIN"]
|
||||
allowPrivilegeEscalation: true
|
||||
image: {{ .Values.node.image.name }}:{{ .Values.node.image.tag }}
|
||||
imagePullPolicy: {{ .Values.node.image.pullPolicy }}
|
||||
args:
|
||||
- "-v=5"
|
||||
- "--nodeid=$(NODE_ID)"
|
||||
- "--endpoint=$(CSI_ENDPOINT)"
|
||||
env:
|
||||
- name: NODE_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: CSI_ENDPOINT
|
||||
value: unix:///csi/csi.sock
|
||||
ports:
|
||||
- containerPort: 29653
|
||||
name: healthz
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
failureThreshold: 5
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: healthz
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 10
|
||||
periodSeconds: 30
|
||||
volumeMounts:
|
||||
- name: socket-dir
|
||||
mountPath: /csi
|
||||
- name: pods-mount-dir
|
||||
mountPath: /var/lib/kubelet/pods
|
||||
mountPropagation: "Bidirectional"
|
||||
resources:
|
||||
limits:
|
||||
memory: {{ .Values.node.resources.limits.memory }}
|
||||
requests:
|
||||
cpu: {{ .Values.node.resources.requests.cpu }}
|
||||
memory: {{ .Values.node.resources.requests.memory }}
|
||||
volumes:
|
||||
- name: socket-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/plugins/webdav-csiplugin
|
||||
type: DirectoryOrCreate
|
||||
- name: pods-mount-dir
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/pods
|
||||
type: Directory
|
||||
- hostPath:
|
||||
path: /var/lib/kubelet/plugins_registry
|
||||
type: Directory
|
||||
name: registration-dir
|
@ -0,0 +1,48 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: webdav-csi-sa
|
||||
namespace: kube-system
|
||||
---
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: webdav-csi-cr
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumes"]
|
||||
verbs: ["get", "list", "watch", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get", "list", "watch", "update"]
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["storageclasses"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["list", "watch", "create", "update", "patch"]
|
||||
- apiGroups: ["storage.k8s.io"]
|
||||
resources: ["csinodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["coordination.k8s.io"]
|
||||
resources: ["leases"]
|
||||
verbs: ["get", "watch", "list", "delete", "update", "create"]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: webdav-csi-crb
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: webdav-csi-sa
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: webdav-csi-cr
|
||||
apiGroup: rbac.authorization.k8s.io
|
@ -0,0 +1,50 @@
|
||||
controller:
|
||||
replicas: 1
|
||||
image:
|
||||
name: ghcr.io/ruakij/webdav-csi-driver
|
||||
tag: v0.0.1
|
||||
pullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
memory: 200Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
|
||||
node:
|
||||
image:
|
||||
name: ghcr.io/ruakij/webdav-csi-driver
|
||||
tag: v0.0.1
|
||||
pullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
memory: 300Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
|
||||
livenessProbe:
|
||||
image:
|
||||
name: registry.k8s.io/sig-storage/livenessprobe
|
||||
tag: v2.11.0
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
csiProvisioner:
|
||||
image:
|
||||
name: registry.k8s.io/sig-storage/csi-provisioner
|
||||
tag: v3.6.2
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Configuration for the default storage class
|
||||
defaultStorageClass: {}
|
||||
# name: "webdav"
|
||||
# parameters:
|
||||
# # alist folder webdav address
|
||||
# share: http://ip:port/dav/media
|
||||
# csi.storage.k8s.io/provisioner-secret-name: "webdav-secrect"
|
||||
# csi.storage.k8s.io/provisioner-secret-namespace: "default"
|
||||
# csi.storage.k8s.io/node-publish-secret-name: "webdav-secrect"
|
||||
# csi.storage.k8s.io/node-publish-secret-namespace: "default"
|
||||
# reclaimPolicy: "Delete"
|
||||
# volumeBindingMode: Immediate
|
||||
# mountOptions: {}
|
@ -0,0 +1,248 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
|
||||
)
|
||||
|
||||
type ControllerServer struct {
|
||||
csi.UnimplementedControllerServer
|
||||
*Driver
|
||||
mounter mount.Interface
|
||||
}
|
||||
|
||||
func NewControllerServer(d *Driver, mounter mount.Interface) *ControllerServer {
|
||||
return &ControllerServer{
|
||||
Driver: d,
|
||||
mounter: mounter,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateVolume implements csi.ControllerServer.
|
||||
func (c *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
|
||||
name := req.GetName()
|
||||
if len(name) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "CreateVolume name must be provided")
|
||||
}
|
||||
if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
mountPermissions := c.Driver.mountPermissions
|
||||
parameters := req.GetParameters()
|
||||
if parameters == nil {
|
||||
parameters = make(map[string]string)
|
||||
}
|
||||
for k, v := range parameters {
|
||||
switch strings.ToLower(k) {
|
||||
case webdavSharePath, pvcNameKey, pvcNamespaceKey, pvNameKey:
|
||||
case mountPermissionsField:
|
||||
if v != "" {
|
||||
var err error
|
||||
if mountPermissions, err = strconv.ParseUint(v, 8, 32); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid mountPermissions %s in storage class", v))
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, status.Errorf(codes.InvalidArgument, fmt.Sprintf("invalid parameter %q in storage class", k))
|
||||
}
|
||||
}
|
||||
|
||||
targetPath := c.workingMountDir
|
||||
sourcePath := req.Parameters[webdavSharePath]
|
||||
notMnt, err := c.mounter.IsLikelyNotMountPoint(targetPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(targetPath, 0750); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
notMnt = true
|
||||
} else {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
if !notMnt {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("target path %s is alredy mounted", targetPath))
|
||||
}
|
||||
|
||||
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
|
||||
if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error()))
|
||||
}
|
||||
|
||||
internalVolumePath := filepath.Join(targetPath, req.Name)
|
||||
if err = os.Mkdir(internalVolumePath, 0777); err != nil && !os.IsExist(err) {
|
||||
return nil, status.Errorf(codes.Internal, "failed to make subdirectory: %v", err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err = c.mounter.Unmount(targetPath); err != nil {
|
||||
klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
if mountPermissions > 0 {
|
||||
// Reset directory permissions because of umask problems
|
||||
if err = os.Chmod(internalVolumePath, os.FileMode(mountPermissions)); err != nil {
|
||||
klog.Warningf("failed to chmod subdirectory: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return &csi.CreateVolumeResponse{
|
||||
Volume: &csi.Volume{
|
||||
VolumeId: MakeVolumeId(sourcePath, req.Name),
|
||||
CapacityBytes: 0, // by setting it to zero, Provisioner will use PVC requested size as PV size
|
||||
VolumeContext: nil,
|
||||
ContentSource: req.GetVolumeContentSource(),
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// DeleteVolume implements csi.ControllerServer.
|
||||
func (c *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if volumeID == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "volume id is empty")
|
||||
}
|
||||
sourcePath, subDir, err := ParseVolumeId(volumeID)
|
||||
if err != nil {
|
||||
// An invalid ID should be treated as doesn't exist
|
||||
klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err)
|
||||
return &csi.DeleteVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
|
||||
targetPath := c.workingMountDir
|
||||
if err := c.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, nil, nil, stdin); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("mount failed: %v", err.Error()))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err = c.mounter.Unmount(targetPath); err != nil {
|
||||
klog.Warningf("failed to unmount targetpath %s: %v", targetPath, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
internalVolumePath := filepath.Join(targetPath, subDir)
|
||||
klog.V(2).Infof("Removing subdirectory at %v", internalVolumePath)
|
||||
if err = os.RemoveAll(internalVolumePath); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete subdirectory: %v", err.Error())
|
||||
}
|
||||
|
||||
return &csi.DeleteVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
// ValidateVolumeCapabilities implements csi.ControllerServer.
|
||||
func (c *ControllerServer) ValidateVolumeCapabilities(_ context.Context, req *csi.ValidateVolumeCapabilitiesRequest) (*csi.ValidateVolumeCapabilitiesResponse, error) {
|
||||
if len(req.GetVolumeId()) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
if err := isValidVolumeCapabilities(req.GetVolumeCapabilities()); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
return &csi.ValidateVolumeCapabilitiesResponse{
|
||||
Confirmed: &csi.ValidateVolumeCapabilitiesResponse_Confirmed{
|
||||
VolumeCapabilities: req.GetVolumeCapabilities(),
|
||||
},
|
||||
Message: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ControllerGetCapabilities implements csi.ControllerServer.
|
||||
func (c *ControllerServer) ControllerGetCapabilities(context.Context, *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
|
||||
return &csi.ControllerGetCapabilitiesResponse{
|
||||
Capabilities: c.Driver.cscap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ControllerExpandVolume implements csi.ControllerServer.
|
||||
func (*ControllerServer) ControllerExpandVolume(context.Context, *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ControllerGetVolume implements csi.ControllerServer.
|
||||
func (*ControllerServer) ControllerGetVolume(context.Context, *csi.ControllerGetVolumeRequest) (*csi.ControllerGetVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ControllerModifyVolume implements csi.ControllerServer.
|
||||
func (*ControllerServer) ControllerModifyVolume(context.Context, *csi.ControllerModifyVolumeRequest) (*csi.ControllerModifyVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ControllerPublishVolume implements csi.ControllerServer.
|
||||
func (*ControllerServer) ControllerPublishVolume(context.Context, *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ControllerUnpublishVolume implements csi.ControllerServer.
|
||||
func (*ControllerServer) ControllerUnpublishVolume(context.Context, *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// CreateSnapshot implements csi.ControllerServer.
|
||||
func (*ControllerServer) CreateSnapshot(context.Context, *csi.CreateSnapshotRequest) (*csi.CreateSnapshotResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// DeleteSnapshot implements csi.ControllerServer.
|
||||
func (*ControllerServer) DeleteSnapshot(context.Context, *csi.DeleteSnapshotRequest) (*csi.DeleteSnapshotResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// GetCapacity implements csi.ControllerServer.
|
||||
func (*ControllerServer) GetCapacity(context.Context, *csi.GetCapacityRequest) (*csi.GetCapacityResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ListSnapshots implements csi.ControllerServer.
|
||||
func (*ControllerServer) ListSnapshots(context.Context, *csi.ListSnapshotsRequest) (*csi.ListSnapshotsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// ListVolumes implements csi.ControllerServer.
|
||||
func (*ControllerServer) ListVolumes(context.Context, *csi.ListVolumesRequest) (*csi.ListVolumesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// isValidVolumeCapabilities validates the given VolumeCapability array is valid
|
||||
func isValidVolumeCapabilities(volCaps []*csi.VolumeCapability) error {
|
||||
if len(volCaps) == 0 {
|
||||
return fmt.Errorf("volume capabilities missing in request")
|
||||
}
|
||||
for _, c := range volCaps {
|
||||
if c.GetBlock() != nil {
|
||||
return fmt.Errorf("block volume capability not supported")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultDriverName = "webdav.csi.io"
|
||||
fstype = "davfs"
|
||||
webdavSharePath = "share"
|
||||
mountPermissionsField = "mountpermissions"
|
||||
pvcNameKey = "csi.storage.k8s.io/pvc/name"
|
||||
pvcNamespaceKey = "csi.storage.k8s.io/pvc/namespace"
|
||||
pvNameKey = "csi.storage.k8s.io/pv/name"
|
||||
secretUsernameKey = "username"
|
||||
secretPasswordKey = "password"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
name string
|
||||
nodeID string
|
||||
endpoint string
|
||||
version string
|
||||
mountPermissions uint64
|
||||
workingMountDir string
|
||||
defaultOnDeletePolicy string
|
||||
|
||||
cscap []*csi.ControllerServiceCapability
|
||||
nscap []*csi.NodeServiceCapability
|
||||
}
|
||||
|
||||
type DriverOpt struct {
|
||||
Name string
|
||||
NodeID string
|
||||
Endpoint string
|
||||
MountPermissions uint64
|
||||
WorkingMountDir string
|
||||
DefaultOnDeletePolicy string
|
||||
}
|
||||
|
||||
func NewDriver(opt *DriverOpt) *Driver {
|
||||
klog.V(2).Infof("Driver: %v version: %v", opt.Name, driverVersion)
|
||||
|
||||
driverName := opt.Name
|
||||
if driverName == "" {
|
||||
driverName = DefaultDriverName
|
||||
}
|
||||
|
||||
driver := &Driver{
|
||||
name: driverName,
|
||||
nodeID: opt.NodeID,
|
||||
endpoint: opt.Endpoint,
|
||||
mountPermissions: opt.MountPermissions,
|
||||
workingMountDir: opt.WorkingMountDir,
|
||||
defaultOnDeletePolicy: opt.DefaultOnDeletePolicy,
|
||||
version: driverName,
|
||||
}
|
||||
|
||||
driver.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{
|
||||
csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
|
||||
csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
|
||||
})
|
||||
|
||||
driver.AddNodeServiceCapabilities([]csi.NodeServiceCapability_RPC_Type{
|
||||
csi.NodeServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER,
|
||||
csi.NodeServiceCapability_RPC_UNKNOWN,
|
||||
})
|
||||
|
||||
return driver
|
||||
}
|
||||
|
||||
func (d *Driver) Run() {
|
||||
versionMeta, err := GetVersionYAML(d.name)
|
||||
if err != nil {
|
||||
klog.Fatalf("%v", err)
|
||||
}
|
||||
klog.V(2).Infof("\nDRIVER INFORMATION:\n-------------------\n%s\n\nStreaming logs below:", versionMeta)
|
||||
|
||||
mounter := mount.New("")
|
||||
server := NewNonBlockingGRPCServer()
|
||||
server.Start(d.endpoint,
|
||||
NewIdentityServer(d),
|
||||
NewControllerServer(d, mounter),
|
||||
NewNodeServer(d, mounter),
|
||||
)
|
||||
server.Wait()
|
||||
}
|
||||
|
||||
func (d *Driver) AddControllerServiceCapabilities(cl []csi.ControllerServiceCapability_RPC_Type) {
|
||||
var csc []*csi.ControllerServiceCapability
|
||||
for _, c := range cl {
|
||||
csc = append(csc, NewControllerServiceCapability(c))
|
||||
}
|
||||
d.cscap = csc
|
||||
}
|
||||
|
||||
func (d *Driver) AddNodeServiceCapabilities(nl []csi.NodeServiceCapability_RPC_Type) {
|
||||
var nsc []*csi.NodeServiceCapability
|
||||
for _, n := range nl {
|
||||
nsc = append(nsc, NewNodeServiceCapability(n))
|
||||
}
|
||||
d.nscap = nsc
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
type IdentityServer struct {
|
||||
csi.UnimplementedIdentityServer
|
||||
Driver *Driver
|
||||
}
|
||||
|
||||
func NewIdentityServer(d *Driver) *IdentityServer {
|
||||
return &IdentityServer{
|
||||
Driver: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (ids *IdentityServer) GetPluginInfo(_ context.Context, _ *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
|
||||
if ids.Driver.name == "" {
|
||||
return nil, status.Error(codes.Unavailable, "Driver name not configured")
|
||||
}
|
||||
|
||||
if ids.Driver.version == "" {
|
||||
return nil, status.Error(codes.Unavailable, "Driver is missing version")
|
||||
}
|
||||
|
||||
return &csi.GetPluginInfoResponse{
|
||||
Name: ids.Driver.name,
|
||||
VendorVersion: ids.Driver.version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ids *IdentityServer) Probe(_ context.Context, _ *csi.ProbeRequest) (*csi.ProbeResponse, error) {
|
||||
return &csi.ProbeResponse{Ready: &wrapperspb.BoolValue{Value: true}}, nil
|
||||
}
|
||||
|
||||
func (ids *IdentityServer) GetPluginCapabilities(_ context.Context, _ *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
|
||||
return &csi.GetPluginCapabilitiesResponse{
|
||||
Capabilities: []*csi.PluginCapability{
|
||||
{
|
||||
Type: &csi.PluginCapability_Service_{
|
||||
Service: &csi.PluginCapability_Service{
|
||||
Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,405 @@
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have
|
||||
// an alternate platform, we will need to abstract further.
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default mount command if mounter path is not specified.
|
||||
defaultMountCommand = "mount"
|
||||
// Log message where sensitive mount options were removed
|
||||
sensitiveOptionsRemoved = "<masked>"
|
||||
)
|
||||
|
||||
// Interface defines the set of methods to allow for mount operations on a system.
|
||||
type Interface interface {
|
||||
// Mount mounts source to target as fstype with given options.
|
||||
// options MUST not contain sensitive material (like passwords).
|
||||
Mount(source string, target string, fstype string, options []string) error
|
||||
// MountSensitive is the same as Mount() but this method allows
|
||||
// sensitiveOptions to be passed in a separate parameter from the normal
|
||||
// mount options and ensures the sensitiveOptions are never logged. This
|
||||
// method should be used by callers that pass sensitive material (like
|
||||
// passwords) as mount options.
|
||||
MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error
|
||||
// MountSensitiveWithStdin
|
||||
MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error
|
||||
// MountSensitiveWithoutSystemd is the same as MountSensitive() but this method disable using systemd mount.
|
||||
MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error
|
||||
// MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd() with additional mount flags
|
||||
MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error
|
||||
// Unmount unmounts given target.
|
||||
Unmount(target string) error
|
||||
// List returns a list of all mounted filesystems. This can be large.
|
||||
// On some platforms, reading mounts directly from the OS is not guaranteed
|
||||
// consistent (i.e. it could change between chunked reads). This is guaranteed
|
||||
// to be consistent.
|
||||
List() ([]MountPoint, error)
|
||||
// IsLikelyNotMountPoint uses heuristics to determine if a directory
|
||||
// is not a mountpoint.
|
||||
// It should return ErrNotExist when the directory does not exist.
|
||||
// IsLikelyNotMountPoint does NOT properly detect all mountpoint types
|
||||
// most notably linux bind mounts and symbolic link. For callers that do not
|
||||
// care about such situations, this is a faster alternative to calling List()
|
||||
// and scanning that output.
|
||||
IsLikelyNotMountPoint(file string) (bool, error)
|
||||
// CanSafelySkipMountPointCheck indicates whether this mounter returns errors on
|
||||
// operations for targets that are not mount points. If this returns true, no such
|
||||
// errors will be returned.
|
||||
CanSafelySkipMountPointCheck() bool
|
||||
// IsMountPoint determines if a directory is a mountpoint.
|
||||
// It should return ErrNotExist when the directory does not exist.
|
||||
// IsMountPoint is more expensive than IsLikelyNotMountPoint.
|
||||
// IsMountPoint detects bind mounts in linux.
|
||||
// IsMountPoint may enumerate all the mountpoints using List() and
|
||||
// the list of mountpoints may be large, then it uses
|
||||
// isMountPointMatch to evaluate whether the directory is a mountpoint.
|
||||
IsMountPoint(file string) (bool, error)
|
||||
// GetMountRefs finds all mount references to pathname, returning a slice of
|
||||
// paths. Pathname can be a mountpoint path or a normal directory
|
||||
// (for bind mount). On Linux, pathname is excluded from the slice.
|
||||
// For example, if /dev/sdc was mounted at /path/a and /path/b,
|
||||
// GetMountRefs("/path/a") would return ["/path/b"]
|
||||
// GetMountRefs("/path/b") would return ["/path/a"]
|
||||
// On Windows there is no way to query all mount points; as long as pathname is
|
||||
// a valid mount, it will be returned.
|
||||
GetMountRefs(pathname string) ([]string, error)
|
||||
}
|
||||
|
||||
// Compile-time check to ensure all Mounter implementations satisfy
|
||||
// the mount interface.
|
||||
var _ Interface = &Mounter{}
|
||||
|
||||
type MounterForceUnmounter interface {
|
||||
Interface
|
||||
// UnmountWithForce unmounts given target but will retry unmounting with force option
|
||||
// after given timeout.
|
||||
UnmountWithForce(target string, umountTimeout time.Duration) error
|
||||
}
|
||||
|
||||
// MountPoint represents a single line in /proc/mounts or /etc/fstab.
|
||||
type MountPoint struct { // nolint: golint
|
||||
Device string
|
||||
Path string
|
||||
Type string
|
||||
Opts []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged).
|
||||
Freq int
|
||||
Pass int
|
||||
}
|
||||
|
||||
type MountErrorType string // nolint: golint
|
||||
|
||||
const (
|
||||
FilesystemMismatch MountErrorType = "FilesystemMismatch"
|
||||
HasFilesystemErrors MountErrorType = "HasFilesystemErrors"
|
||||
UnformattedReadOnly MountErrorType = "UnformattedReadOnly"
|
||||
FormatFailed MountErrorType = "FormatFailed"
|
||||
GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed"
|
||||
UnknownMountError MountErrorType = "UnknownMountError"
|
||||
)
|
||||
|
||||
type MountError struct { // nolint: golint
|
||||
Type MountErrorType
|
||||
Message string
|
||||
}
|
||||
|
||||
func (mountError MountError) String() string {
|
||||
return mountError.Message
|
||||
}
|
||||
|
||||
func (mountError MountError) Error() string {
|
||||
return mountError.Message
|
||||
}
|
||||
|
||||
func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error {
|
||||
mountError := MountError{
|
||||
Type: mountErrorValue,
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
}
|
||||
return mountError
|
||||
}
|
||||
|
||||
// SafeFormatAndMount probes a device to see if it is formatted.
|
||||
// Namely it checks to see if a file system is present. If so it
|
||||
// mounts it otherwise the device is formatted first then mounted.
|
||||
type SafeFormatAndMount struct {
|
||||
Interface
|
||||
Exec utilexec.Interface
|
||||
|
||||
formatSem chan any
|
||||
formatTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewSafeFormatAndMount(mounter Interface, exec utilexec.Interface, opts ...Option) *SafeFormatAndMount {
|
||||
res := &SafeFormatAndMount{
|
||||
Interface: mounter,
|
||||
Exec: exec,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type Option func(*SafeFormatAndMount)
|
||||
|
||||
// WithMaxConcurrentFormat sets the maximum number of concurrent format
|
||||
// operations executed by the mounter. The timeout controls the maximum
|
||||
// duration of a format operation before its concurrency token is released.
|
||||
// Once a token is released, it can be acquired by another concurrent format
|
||||
// operation. The original operation is allowed to complete.
|
||||
// If n < 1, concurrency is set to unlimited.
|
||||
func WithMaxConcurrentFormat(n int, timeout time.Duration) Option {
|
||||
return func(mounter *SafeFormatAndMount) {
|
||||
if n > 0 {
|
||||
mounter.formatSem = make(chan any, n)
|
||||
mounter.formatTimeout = timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FormatAndMount formats the given disk, if needed, and mounts it.
|
||||
// That is if the disk is not formatted and it is not being mounted as
|
||||
// read-only it will format it first then mount it. Otherwise, if the
|
||||
// disk is already formatted or it is being mounted as read-only, it
|
||||
// will be mounted without formatting.
|
||||
// options MUST not contain sensitive material (like passwords).
|
||||
func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error {
|
||||
return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
|
||||
}
|
||||
|
||||
// FormatAndMountSensitive is the same as FormatAndMount but this method allows
|
||||
// sensitiveOptions to be passed in a separate parameter from the normal mount
|
||||
// options and ensures the sensitiveOptions are never logged. This method should
|
||||
// be used by callers that pass sensitive material (like passwords) as mount
|
||||
// options.
|
||||
func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
|
||||
return mounter.FormatAndMountSensitiveWithFormatOptions(source, target, fstype, options, sensitiveOptions, nil /* formatOptions */)
|
||||
}
|
||||
|
||||
// FormatAndMountSensitiveWithFormatOptions behaves exactly the same as
|
||||
// FormatAndMountSensitive, but allows for options to be passed when the disk
|
||||
// is formatted. These options are NOT validated in any way and should never
|
||||
// come directly from untrusted user input as that would be an injection risk.
|
||||
func (mounter *SafeFormatAndMount) FormatAndMountSensitiveWithFormatOptions(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error {
|
||||
return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions, formatOptions)
|
||||
}
|
||||
|
||||
// getMountRefsByDev finds all references to the device provided
|
||||
// by mountPath; returns a list of paths.
|
||||
// Note that mountPath should be path after the evaluation of any symblolic links.
|
||||
//
|
||||
//lint:ignore U1000 Ignore unused function temporarily for debugging
|
||||
func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) {
|
||||
mps, err := mounter.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finding the device mounted to mountPath.
|
||||
diskDev := ""
|
||||
for i := range mps {
|
||||
if mountPath == mps[i].Path {
|
||||
diskDev = mps[i].Device
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all references to the device.
|
||||
var refs []string
|
||||
for i := range mps {
|
||||
if mps[i].Device == diskDev || mps[i].Device == mountPath {
|
||||
if mps[i].Path != mountPath {
|
||||
refs = append(refs, mps[i].Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
// IsNotMountPoint determines if a directory is a mountpoint.
|
||||
// It should return ErrNotExist when the directory does not exist.
|
||||
// IsNotMountPoint is more expensive than IsLikelyNotMountPoint
|
||||
// and depends on IsMountPoint.
|
||||
//
|
||||
// If an error occurs, it returns true (assuming it is not a mountpoint)
|
||||
// when ErrNotExist is returned for callers similar to IsLikelyNotMountPoint.
|
||||
//
|
||||
// Deprecated: This function is kept to keep changes backward compatible with
|
||||
// previous library version. Callers should prefer mounter.IsMountPoint.
|
||||
func IsNotMountPoint(mounter Interface, file string) (bool, error) {
|
||||
isMnt, err := mounter.IsMountPoint(file)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
return !isMnt, nil
|
||||
}
|
||||
|
||||
// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts
|
||||
// returns the device name, reference count, and error code.
|
||||
func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) {
|
||||
mps, err := mounter.List()
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
// Find the device name.
|
||||
// FIXME if multiple devices mounted on the same mount path, only the first one is returned.
|
||||
device := ""
|
||||
// If mountPath is symlink, need get its target path.
|
||||
slTarget, err := filepath.EvalSymlinks(mountPath)
|
||||
if err != nil {
|
||||
slTarget = mountPath
|
||||
}
|
||||
for i := range mps {
|
||||
if mps[i].Path == slTarget {
|
||||
device = mps[i].Device
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find all references to the device.
|
||||
refCount := 0
|
||||
for i := range mps {
|
||||
if mps[i].Device == device {
|
||||
refCount++
|
||||
}
|
||||
}
|
||||
return device, refCount, nil
|
||||
}
|
||||
|
||||
// MakeBindOpts detects whether a bind mount is being requested and makes the remount options to
|
||||
// use in case of bind mount, due to the fact that bind mount doesn't respect mount options.
|
||||
// The list equals:
|
||||
//
|
||||
// options - 'bind' + 'remount' (no duplicate)
|
||||
func MakeBindOpts(options []string) (bool, []string, []string) {
|
||||
bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */)
|
||||
return bind, bindOpts, bindRemountOpts
|
||||
}
|
||||
|
||||
// MakeBindOptsSensitive is the same as MakeBindOpts but this method allows
|
||||
// sensitiveOptions to be passed in a separate parameter from the normal mount
|
||||
// options and ensures the sensitiveOptions are never logged. This method should
|
||||
// be used by callers that pass sensitive material (like passwords) as mount
|
||||
// options.
|
||||
func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) {
|
||||
// Because we have an FD opened on the subpath bind mount, the "bind" option
|
||||
// needs to be included, otherwise the mount target will error as busy if you
|
||||
// remount as readonly.
|
||||
//
|
||||
// As a consequence, all read only bind mounts will no longer change the underlying
|
||||
// volume mount to be read only.
|
||||
bindRemountOpts := []string{"bind", "remount"}
|
||||
bindRemountSensitiveOpts := []string{}
|
||||
bind := false
|
||||
bindOpts := []string{"bind"}
|
||||
|
||||
// _netdev is a userspace mount option and does not automatically get added when
|
||||
// bind mount is created and hence we must carry it over.
|
||||
if checkForNetDev(options, sensitiveOptions) {
|
||||
bindOpts = append(bindOpts, "_netdev")
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
switch option {
|
||||
case "bind":
|
||||
bind = true
|
||||
case "remount":
|
||||
default:
|
||||
bindRemountOpts = append(bindRemountOpts, option)
|
||||
}
|
||||
}
|
||||
|
||||
for _, sensitiveOption := range sensitiveOptions {
|
||||
switch sensitiveOption {
|
||||
case "bind":
|
||||
bind = true
|
||||
case "remount":
|
||||
default:
|
||||
bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption)
|
||||
}
|
||||
}
|
||||
|
||||
return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts
|
||||
}
|
||||
|
||||
func checkForNetDev(options []string, sensitiveOptions []string) bool {
|
||||
for _, option := range options {
|
||||
if option == "_netdev" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, sensitiveOption := range sensitiveOptions {
|
||||
if sensitiveOption == "_netdev" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PathWithinBase checks if give path is within given base directory.
|
||||
func PathWithinBase(fullPath, basePath string) bool {
|
||||
rel, err := filepath.Rel(basePath, fullPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if StartsWithBackstep(rel) {
|
||||
// Needed to escape the base path.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// StartsWithBackstep checks if the given path starts with a backstep segment.
|
||||
func StartsWithBackstep(rel string) bool {
|
||||
// normalize to / and check for ../
|
||||
return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../")
|
||||
}
|
||||
|
||||
// sanitizedOptionsForLogging will return a comma separated string containing
|
||||
// options and sensitiveOptions. Each entry in sensitiveOptions will be
|
||||
// replaced with the string sensitiveOptionsRemoved
|
||||
// e.g. o1,o2,<masked>,<masked>
|
||||
func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string {
|
||||
separator := ""
|
||||
if len(options) > 0 && len(sensitiveOptions) > 0 {
|
||||
separator = ","
|
||||
}
|
||||
|
||||
sensitiveOptionsStart := ""
|
||||
sensitiveOptionsEnd := ""
|
||||
if len(sensitiveOptions) > 0 {
|
||||
sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1)
|
||||
sensitiveOptionsEnd = sensitiveOptionsRemoved
|
||||
}
|
||||
|
||||
return strings.Join(options, ",") +
|
||||
separator +
|
||||
sensitiveOptionsStart +
|
||||
sensitiveOptionsEnd
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// CleanupMountPoint unmounts the given path and deletes the remaining directory
|
||||
// if successful. If extensiveMountPointCheck is true IsNotMountPoint will be
|
||||
// called instead of IsLikelyNotMountPoint. IsNotMountPoint is more expensive
|
||||
// but properly handles bind mounts within the same fs.
|
||||
func CleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) error {
|
||||
pathExists, pathErr := PathExists(mountPath)
|
||||
if !pathExists && pathErr == nil {
|
||||
klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath)
|
||||
return nil
|
||||
}
|
||||
corruptedMnt := IsCorruptedMnt(pathErr)
|
||||
if pathErr != nil && !corruptedMnt {
|
||||
return fmt.Errorf("Error checking path: %v", pathErr)
|
||||
}
|
||||
return doCleanupMountPoint(mountPath, mounter, extensiveMountPointCheck, corruptedMnt)
|
||||
}
|
||||
|
||||
func CleanupMountWithForce(mountPath string, mounter MounterForceUnmounter, extensiveMountPointCheck bool, umountTimeout time.Duration) error {
|
||||
pathExists, pathErr := PathExists(mountPath)
|
||||
if !pathExists && pathErr == nil {
|
||||
klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath)
|
||||
return nil
|
||||
}
|
||||
corruptedMnt := IsCorruptedMnt(pathErr)
|
||||
if pathErr != nil && !corruptedMnt {
|
||||
return fmt.Errorf("Error checking path: %v", pathErr)
|
||||
}
|
||||
|
||||
if corruptedMnt || mounter.CanSafelySkipMountPointCheck() {
|
||||
klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)",
|
||||
mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck())
|
||||
if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
return removePath(mountPath)
|
||||
}
|
||||
|
||||
notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
|
||||
// if mountPath is not a mount point, it's just been removed or there was an error
|
||||
if err != nil || notMnt {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath)
|
||||
if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
|
||||
// if mountPath is not a mount point, it's either just been removed or there was an error
|
||||
if notMnt {
|
||||
return err
|
||||
}
|
||||
// mountPath is still a mount point
|
||||
return fmt.Errorf("failed to cleanup mount point %v", mountPath)
|
||||
}
|
||||
|
||||
// doCleanupMountPoint unmounts the given path and
|
||||
// deletes the remaining directory if successful.
|
||||
// if extensiveMountPointCheck is true
|
||||
// IsNotMountPoint will be called instead of IsLikelyNotMountPoint.
|
||||
// IsNotMountPoint is more expensive but properly handles bind mounts within the same fs.
|
||||
// if corruptedMnt is true, it means that the mountPath is a corrupted mountpoint, and the mount point check
|
||||
// will be skipped. The mount point check will also be skipped if the mounter supports it.
|
||||
func doCleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool, corruptedMnt bool) error {
|
||||
if corruptedMnt || mounter.CanSafelySkipMountPointCheck() {
|
||||
klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)",
|
||||
mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck())
|
||||
if err := mounter.Unmount(mountPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return removePath(mountPath)
|
||||
}
|
||||
|
||||
notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
|
||||
// if mountPath is not a mount point, it's just been removed or there was an error
|
||||
if err != nil || notMnt {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath)
|
||||
if err := mounter.Unmount(mountPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck)
|
||||
// if mountPath is not a mount point, it's either just been removed or there was an error
|
||||
if notMnt {
|
||||
return err
|
||||
}
|
||||
// mountPath is still a mount point
|
||||
return fmt.Errorf("failed to cleanup mount point %v", mountPath)
|
||||
}
|
||||
|
||||
// removePathIfNotMountPoint verifies if given mountPath is a mount point if not it attempts
|
||||
// to remove the directory. Returns true and nil if directory was not a mount point and removed.
|
||||
func removePathIfNotMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) (bool, error) {
|
||||
var notMnt bool
|
||||
var err error
|
||||
|
||||
if extensiveMountPointCheck {
|
||||
notMnt, err = IsNotMountPoint(mounter, mountPath)
|
||||
} else {
|
||||
notMnt, err = mounter.IsLikelyNotMountPoint(mountPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
klog.V(4).Infof("%q does not exist", mountPath)
|
||||
return true, nil
|
||||
}
|
||||
return notMnt, err
|
||||
}
|
||||
|
||||
if notMnt {
|
||||
klog.Warningf("Warning: %q is not a mountpoint, deleting", mountPath)
|
||||
return notMnt, os.Remove(mountPath)
|
||||
}
|
||||
return notMnt, nil
|
||||
}
|
||||
|
||||
// removePath attempts to remove the directory. Returns nil if the directory was removed or does not exist.
|
||||
func removePath(mountPath string) error {
|
||||
klog.V(4).Infof("Warning: deleting path %q", mountPath)
|
||||
err := os.Remove(mountPath)
|
||||
if os.IsNotExist(err) {
|
||||
klog.V(4).Infof("%q does not exist", mountPath)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,250 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"k8s.io/klog/v2"
|
||||
utilio "k8s.io/utils/io"
|
||||
)
|
||||
|
||||
const (
|
||||
// At least number of fields per line in /proc/<pid>/mountinfo.
|
||||
expectedAtLeastNumFieldsPerMountInfo = 10
|
||||
// How many times to retry for a consistent read of /proc/mounts.
|
||||
maxListTries = 10
|
||||
)
|
||||
|
||||
// IsCorruptedMnt return true if err is about corrupted mount point
|
||||
func IsCorruptedMnt(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var underlyingError error
|
||||
switch pe := err.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case *os.PathError:
|
||||
underlyingError = pe.Err
|
||||
case *os.LinkError:
|
||||
underlyingError = pe.Err
|
||||
case *os.SyscallError:
|
||||
underlyingError = pe.Err
|
||||
case syscall.Errno:
|
||||
underlyingError = err
|
||||
}
|
||||
|
||||
return underlyingError == syscall.ENOTCONN || underlyingError == syscall.ESTALE || underlyingError == syscall.EIO || underlyingError == syscall.EACCES || underlyingError == syscall.EHOSTDOWN
|
||||
}
|
||||
|
||||
// MountInfo represents a single line in /proc/<pid>/mountinfo.
|
||||
type MountInfo struct { // nolint: golint
|
||||
// Unique ID for the mount (maybe reused after umount).
|
||||
ID int
|
||||
// The ID of the parent mount (or of self for the root of this mount namespace's mount tree).
|
||||
ParentID int
|
||||
// Major indicates one half of the device ID which identifies the device class
|
||||
// (parsed from `st_dev` for files on this filesystem).
|
||||
Major int
|
||||
// Minor indicates one half of the device ID which identifies a specific
|
||||
// instance of device (parsed from `st_dev` for files on this filesystem).
|
||||
Minor int
|
||||
// The pathname of the directory in the filesystem which forms the root of this mount.
|
||||
Root string
|
||||
// Mount source, filesystem-specific information. e.g. device, tmpfs name.
|
||||
Source string
|
||||
// Mount point, the pathname of the mount point.
|
||||
MountPoint string
|
||||
// Optional fieds, zero or more fields of the form "tag[:value]".
|
||||
OptionalFields []string
|
||||
// The filesystem type in the form "type[.subtype]".
|
||||
FsType string
|
||||
// Per-mount options.
|
||||
MountOptions []string
|
||||
// Per-superblock options.
|
||||
SuperOptions []string
|
||||
}
|
||||
|
||||
// ParseMountInfo parses /proc/xxx/mountinfo.
|
||||
func ParseMountInfo(filename string) ([]MountInfo, error) {
|
||||
content, err := readMountInfo(filename)
|
||||
if err != nil {
|
||||
return []MountInfo{}, err
|
||||
}
|
||||
contentStr := string(content)
|
||||
infos := []MountInfo{}
|
||||
|
||||
for _, line := range strings.Split(contentStr, "\n") {
|
||||
if line == "" {
|
||||
// the last split() item is empty string following the last \n
|
||||
continue
|
||||
}
|
||||
// See `man proc` for authoritative description of format of the file.
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < expectedAtLeastNumFieldsPerMountInfo {
|
||||
return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line)
|
||||
}
|
||||
id, err := strconv.Atoi(fields[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentID, err := strconv.Atoi(fields[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mm := strings.Split(fields[2], ":")
|
||||
if len(mm) != 2 {
|
||||
return nil, fmt.Errorf("parsing '%s' failed: unexpected minor:major pair %s", line, mm)
|
||||
}
|
||||
major, err := strconv.Atoi(mm[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing '%s' failed: unable to parse major device id, err:%v", mm[0], err)
|
||||
}
|
||||
minor, err := strconv.Atoi(mm[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing '%s' failed: unable to parse minor device id, err:%v", mm[1], err)
|
||||
}
|
||||
|
||||
info := MountInfo{
|
||||
ID: id,
|
||||
ParentID: parentID,
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Root: fields[3],
|
||||
MountPoint: fields[4],
|
||||
MountOptions: splitMountOptions(fields[5]),
|
||||
}
|
||||
// All fields until "-" are "optional fields".
|
||||
i := 6
|
||||
for ; i < len(fields) && fields[i] != "-"; i++ {
|
||||
info.OptionalFields = append(info.OptionalFields, fields[i])
|
||||
}
|
||||
// Parse the rest 3 fields.
|
||||
i++
|
||||
if len(fields)-i < 3 {
|
||||
return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i)
|
||||
}
|
||||
info.FsType = fields[i]
|
||||
info.Source = fields[i+1]
|
||||
info.SuperOptions = splitMountOptions(fields[i+2])
|
||||
infos = append(infos, info)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// splitMountOptions parses comma-separated list of mount options into an array.
|
||||
// It respects double quotes - commas in them are not considered as the option separator.
|
||||
func splitMountOptions(s string) []string {
|
||||
inQuotes := false
|
||||
list := strings.FieldsFunc(s, func(r rune) bool {
|
||||
if r == '"' {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
// Report a new field only when outside of double quotes.
|
||||
return r == ',' && !inQuotes
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
// isMountPointMatch returns true if the path in mp is the same as dir.
|
||||
// Handles case where mountpoint dir has been renamed due to stale NFS mount.
|
||||
func isMountPointMatch(mp MountPoint, dir string) bool {
|
||||
return strings.TrimSuffix(mp.Path, "\\040(deleted)") == dir
|
||||
}
|
||||
|
||||
// PathExists returns true if the specified path exists.
|
||||
// TODO: clean this up to use pkg/util/file/FileExists
|
||||
func PathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
} else if errors.Is(err, fs.ErrNotExist) {
|
||||
err = syscall.Access(path, syscall.F_OK)
|
||||
if err == nil {
|
||||
// The access syscall says the file exists, the stat syscall says it
|
||||
// doesn't. This was observed on CIFS when the path was removed at
|
||||
// the server somehow. POSIX calls this a stale file handle, let's fake
|
||||
// that error and treat the path as existing but corrupted.
|
||||
klog.Warningf("Potential stale file handle detected: %s", path)
|
||||
return true, syscall.ESTALE
|
||||
}
|
||||
return false, nil
|
||||
} else if IsCorruptedMnt(err) {
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// These variables are used solely by kernelHasMountinfoBug.
|
||||
var (
|
||||
hasMountinfoBug bool
|
||||
checkMountinfoBugOnce sync.Once
|
||||
)
|
||||
|
||||
// kernelHasMountinfoBug checks if the kernel bug that can lead to incomplete
|
||||
// mountinfo being read is fixed. It does so by checking the kernel version.
|
||||
//
|
||||
// The bug was fixed by the kernel commit 9f6c61f96f2d97 (since Linux 5.8).
|
||||
// Alas, there is no better way to check if the bug is fixed other than to
|
||||
// rely on the kernel version returned by uname.
|
||||
func kernelHasMountinfoBug() bool {
|
||||
checkMountinfoBugOnce.Do(func() {
|
||||
// Assume old kernel.
|
||||
hasMountinfoBug = true
|
||||
|
||||
uname := unix.Utsname{}
|
||||
err := unix.Uname(&uname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
end := bytes.IndexByte(uname.Release[:], 0)
|
||||
v := bytes.SplitN(uname.Release[:end], []byte{'.'}, 3)
|
||||
if len(v) != 3 {
|
||||
return
|
||||
}
|
||||
major, _ := strconv.Atoi(string(v[0]))
|
||||
minor, _ := strconv.Atoi(string(v[1]))
|
||||
|
||||
if major > 5 || (major == 5 && minor >= 8) {
|
||||
hasMountinfoBug = false
|
||||
}
|
||||
})
|
||||
|
||||
return hasMountinfoBug
|
||||
}
|
||||
|
||||
func readMountInfo(path string) ([]byte, error) {
|
||||
if kernelHasMountinfoBug() {
|
||||
return utilio.ConsistentRead(path, maxListTries)
|
||||
}
|
||||
|
||||
return os.ReadFile(path)
|
||||
}
|
@ -0,0 +1,871 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
utilexec "k8s.io/utils/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
// Number of fields per line in /proc/mounts as per the fstab man page.
|
||||
expectedNumFieldsPerLine = 6
|
||||
// Location of the mount file to use
|
||||
procMountsPath = "/proc/mounts"
|
||||
// Location of the mountinfo file
|
||||
procMountInfoPath = "/proc/self/mountinfo"
|
||||
// 'fsck' found errors and corrected them
|
||||
fsckErrorsCorrected = 1
|
||||
// 'fsck' found errors but exited without correcting them
|
||||
fsckErrorsUncorrected = 4
|
||||
// Error thrown by exec cmd.Run() when process spawned by cmd.Start() completes before cmd.Wait() is called (see - k/k issue #103753)
|
||||
errNoChildProcesses = "wait: no child processes"
|
||||
// Error returned by some `umount` implementations when the specified path is not a mount point
|
||||
errNotMounted = "not mounted"
|
||||
)
|
||||
|
||||
// Mounter provides the default implementation of mount.Interface
|
||||
// for the linux platform. This implementation assumes that the
|
||||
// kubelet is running in the host's root mount namespace.
|
||||
type Mounter struct {
|
||||
mounterPath string
|
||||
withSystemd *bool
|
||||
trySystemd bool
|
||||
withSafeNotMountedBehavior bool
|
||||
}
|
||||
|
||||
var _ MounterForceUnmounter = &Mounter{}
|
||||
|
||||
// New returns a mount.Interface for the current system.
|
||||
// It provides options to override the default mounter behavior.
|
||||
// mounterPath allows using an alternative to `/bin/mount` for mounting.
|
||||
func New(mounterPath string) Interface {
|
||||
return &Mounter{
|
||||
mounterPath: mounterPath,
|
||||
trySystemd: true,
|
||||
withSafeNotMountedBehavior: detectSafeNotMountedBehavior(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewWithoutSystemd returns a Linux specific mount.Interface for the current
|
||||
// system. It provides options to override the default mounter behavior.
|
||||
// mounterPath allows using an alternative to `/bin/mount` for mounting. Any
|
||||
// detection for systemd functionality is disabled with this Mounter.
|
||||
func NewWithoutSystemd(mounterPath string) Interface {
|
||||
return &Mounter{
|
||||
mounterPath: mounterPath,
|
||||
trySystemd: false,
|
||||
withSafeNotMountedBehavior: detectSafeNotMountedBehavior(),
|
||||
}
|
||||
}
|
||||
|
||||
// hasSystemd validates that the withSystemd bool is set, if it is not,
|
||||
// detectSystemd will be called once for this Mounter instance.
|
||||
func (mounter *Mounter) hasSystemd() bool {
|
||||
if !mounter.trySystemd {
|
||||
mounter.withSystemd = &mounter.trySystemd
|
||||
}
|
||||
|
||||
if mounter.withSystemd == nil {
|
||||
withSystemd := detectSystemd()
|
||||
mounter.withSystemd = &withSystemd
|
||||
}
|
||||
|
||||
return *mounter.withSystemd
|
||||
}
|
||||
|
||||
// Mount mounts source to target as fstype with given options. 'source' and 'fstype' must
|
||||
// be an empty string in case it's not required, e.g. for remount, or for auto filesystem
|
||||
// type, where kernel handles fstype for you. The mount 'options' is a list of options,
|
||||
// currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is
|
||||
// required, call Mount with an empty string list or nil.
|
||||
func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error {
|
||||
return mounter.MountSensitive(source, target, fstype, options, nil)
|
||||
}
|
||||
|
||||
// MountSensitive is the same as Mount() but this method allows
|
||||
// sensitiveOptions to be passed in a separate parameter from the normal
|
||||
// mount options and ensures the sensitiveOptions are never logged. This
|
||||
// method should be used by callers that pass sensitive material (like
|
||||
// passwords) as mount options.
|
||||
func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
|
||||
// Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty.
|
||||
// All Linux distros are expected to be shipped with a mount utility that a support bind mounts.
|
||||
mounterPath := ""
|
||||
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
|
||||
if bind {
|
||||
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, nil)
|
||||
}
|
||||
// The list of filesystems that require containerized mounter on GCI image cluster
|
||||
fsTypesNeedMounter := map[string]struct{}{
|
||||
"nfs": {},
|
||||
"glusterfs": {},
|
||||
"ceph": {},
|
||||
"cifs": {},
|
||||
}
|
||||
if _, ok := fsTypesNeedMounter[fstype]; ok {
|
||||
mounterPath = mounter.mounterPath
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, nil)
|
||||
}
|
||||
|
||||
func (mounter *Mounter) MountSensitiveWithStdin(source string, target string, fstype string, options []string, sensitiveOptions []string, stdin []string) error {
|
||||
// Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty.
|
||||
// All Linux distros are expected to be shipped with a mount utility that a support bind mounts.
|
||||
mounterPath := ""
|
||||
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
|
||||
if bind {
|
||||
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd, stdin)
|
||||
}
|
||||
// The list of filesystems that require containerized mounter on GCI image cluster
|
||||
fsTypesNeedMounter := map[string]struct{}{
|
||||
"nfs": {},
|
||||
"glusterfs": {},
|
||||
"ceph": {},
|
||||
"cifs": {},
|
||||
}
|
||||
if _, ok := fsTypesNeedMounter[fstype]; ok {
|
||||
mounterPath = mounter.mounterPath
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd, stdin)
|
||||
}
|
||||
|
||||
// MountSensitiveWithoutSystemd is the same as MountSensitive() but disable using systemd mount.
|
||||
func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
|
||||
return mounter.MountSensitiveWithoutSystemdWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */)
|
||||
}
|
||||
|
||||
// MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd with additional mount flags.
|
||||
func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error {
|
||||
mounterPath := ""
|
||||
bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions)
|
||||
if bind {
|
||||
err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOptsSensitive, mountFlags, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, mountFlags, false, nil)
|
||||
}
|
||||
// The list of filesystems that require containerized mounter on GCI image cluster
|
||||
fsTypesNeedMounter := map[string]struct{}{
|
||||
"nfs": {},
|
||||
"glusterfs": {},
|
||||
"ceph": {},
|
||||
"cifs": {},
|
||||
}
|
||||
if _, ok := fsTypesNeedMounter[fstype]; ok {
|
||||
mounterPath = mounter.mounterPath
|
||||
}
|
||||
return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, mountFlags, false, nil)
|
||||
}
|
||||
|
||||
// doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used.
|
||||
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
|
||||
// systemdMountRequired is an extension of option to decide whether uses systemd mount.
|
||||
func (mounter *Mounter) doMount(mounterPath string, mountCmd string, source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string, systemdMountRequired bool, stdin []string) error {
|
||||
mountArgs, mountArgsLogStr := MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, mountFlags)
|
||||
if len(mounterPath) > 0 {
|
||||
mountArgs = append([]string{mountCmd}, mountArgs...)
|
||||
mountArgsLogStr = mountCmd + " " + mountArgsLogStr
|
||||
mountCmd = mounterPath
|
||||
}
|
||||
|
||||
if systemdMountRequired && mounter.hasSystemd() {
|
||||
// Try to run mount via systemd-run --scope. This will escape the
|
||||
// service where kubelet runs and any fuse daemons will be started in a
|
||||
// specific scope. kubelet service than can be restarted without killing
|
||||
// these fuse daemons.
|
||||
//
|
||||
// Complete command line (when mounterPath is not used):
|
||||
// systemd-run --description=... --scope -- mount -t <type> <what> <where>
|
||||
//
|
||||
// Expected flow:
|
||||
// * systemd-run creates a transient scope (=~ cgroup) and executes its
|
||||
// argument (/bin/mount) there.
|
||||
// * mount does its job, forks a fuse daemon if necessary and finishes.
|
||||
// (systemd-run --scope finishes at this point, returning mount's exit
|
||||
// code and stdout/stderr - thats one of --scope benefits).
|
||||
// * systemd keeps the fuse daemon running in the scope (i.e. in its own
|
||||
// cgroup) until the fuse daemon dies (another --scope benefit).
|
||||
// Kubelet service can be restarted and the fuse daemon survives.
|
||||
// * When the fuse daemon dies (e.g. during unmount) systemd removes the
|
||||
// scope automatically.
|
||||
//
|
||||
// systemd-mount is not used because it's too new for older distros
|
||||
// (CentOS 7, Debian Jessie).
|
||||
mountCmd, mountArgs, mountArgsLogStr = AddSystemdScopeSensitive("systemd-run", target, mountCmd, mountArgs, mountArgsLogStr)
|
||||
// } else {
|
||||
// No systemd-run on the host (or we failed to check it), assume kubelet
|
||||
// does not run as a systemd service.
|
||||
// No code here, mountCmd and mountArgs are already populated.
|
||||
}
|
||||
|
||||
// Logging with sensitive mount options removed.
|
||||
klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgsLogStr)
|
||||
command := exec.Command(mountCmd, mountArgs...)
|
||||
if stdin != nil {
|
||||
writer, err := command.StdinPipe()
|
||||
if err != nil {
|
||||
klog.Errorf("Create stdin pipe failed: %v\nMounting command: %s\nMounting arguments: %s\n", err, mountCmd, mountArgsLogStr)
|
||||
return fmt.Errorf("create stdin pip failed: %v\nMounting command: %s\nMounting arguments: %s", err, mountCmd, mountArgsLogStr)
|
||||
}
|
||||
for _, v := range stdin {
|
||||
io.WriteString(writer, v)
|
||||
io.WriteString(writer, "\n")
|
||||
}
|
||||
writer.Close()
|
||||
}
|
||||
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
if err.Error() == errNoChildProcesses {
|
||||
if command.ProcessState.Success() {
|
||||
// We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753).
|
||||
return nil
|
||||
}
|
||||
// Rewrite err with the actual exit error of the process.
|
||||
err = &exec.ExitError{ProcessState: command.ProcessState}
|
||||
}
|
||||
klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, mountArgsLogStr, string(output))
|
||||
return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s",
|
||||
err, mountCmd, mountArgsLogStr, string(output))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// detectSystemd returns true if OS runs with systemd as init. When not sure
|
||||
// (permission errors, ...), it returns false.
|
||||
// There may be different ways how to detect systemd, this one makes sure that
|
||||
// systemd-runs (needed by Mount()) works.
|
||||
func detectSystemd() bool {
|
||||
if _, err := exec.LookPath("systemd-run"); err != nil {
|
||||
klog.V(2).Infof("Detected OS without systemd")
|
||||
return false
|
||||
}
|
||||
// Try to run systemd-run --scope /bin/true, that should be enough
|
||||
// to make sure that systemd is really running and not just installed,
|
||||
// which happens when running in a container with a systemd-based image
|
||||
// but with different pid 1.
|
||||
cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS")
|
||||
klog.V(4).Infof("systemd-run output: %s, failed with: %v", string(output), err)
|
||||
return false
|
||||
}
|
||||
klog.V(2).Infof("Detected OS with systemd")
|
||||
return true
|
||||
}
|
||||
|
||||
// detectSafeNotMountedBehavior returns true if the umount implementation replies "not mounted"
|
||||
// when the specified path is not mounted. When not sure (permission errors, ...), it returns false.
|
||||
// When possible, we will trust umount's message and avoid doing our own mount point checks.
|
||||
// More info: https://github.com/util-linux/util-linux/blob/v2.2/mount/umount.c#L179
|
||||
func detectSafeNotMountedBehavior() bool {
|
||||
return detectSafeNotMountedBehaviorWithExec(utilexec.New())
|
||||
}
|
||||
|
||||
// detectSafeNotMountedBehaviorWithExec is for testing with FakeExec.
|
||||
func detectSafeNotMountedBehaviorWithExec(exec utilexec.Interface) bool {
|
||||
// create a temp dir and try to umount it
|
||||
path, err := os.MkdirTemp("", "kubelet-detect-safe-umount")
|
||||
if err != nil {
|
||||
klog.V(4).Infof("Cannot create temp dir to detect safe 'not mounted' behavior: %v", err)
|
||||
return false
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
cmd := exec.Command("umount", path)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if strings.Contains(string(output), errNotMounted) {
|
||||
klog.V(4).Infof("Detected umount with safe 'not mounted' behavior")
|
||||
return true
|
||||
}
|
||||
klog.V(4).Infof("'umount %s' failed with: %v, output: %s", path, err, string(output))
|
||||
}
|
||||
klog.V(4).Infof("Detected umount with unsafe 'not mounted' behavior")
|
||||
return false
|
||||
}
|
||||
|
||||
// MakeMountArgs makes the arguments to the mount(8) command.
|
||||
// options MUST not contain sensitive material (like passwords).
|
||||
func MakeMountArgs(source, target, fstype string, options []string) (mountArgs []string) {
|
||||
mountArgs, _ = MakeMountArgsSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
|
||||
return mountArgs
|
||||
}
|
||||
|
||||
// MakeMountArgsSensitive makes the arguments to the mount(8) command.
|
||||
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
|
||||
func MakeMountArgsSensitive(source, target, fstype string, options []string, sensitiveOptions []string) (mountArgs []string, mountArgsLogStr string) {
|
||||
return MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */)
|
||||
}
|
||||
|
||||
// MakeMountArgsSensitiveWithMountFlags makes the arguments to the mount(8) command.
|
||||
// sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material)
|
||||
// mountFlags are additional mount flags that are not related with the fstype
|
||||
// and mount options
|
||||
func MakeMountArgsSensitiveWithMountFlags(source, target, fstype string, options []string, sensitiveOptions []string, mountFlags []string) (mountArgs []string, mountArgsLogStr string) {
|
||||
// Build mount command as follows:
|
||||
// mount [$mountFlags] [-t $fstype] [-o $options] [$source] $target
|
||||
mountArgs = []string{}
|
||||
mountArgsLogStr = ""
|
||||
|
||||
mountArgs = append(mountArgs, mountFlags...)
|
||||
mountArgsLogStr += strings.Join(mountFlags, " ")
|
||||
|
||||
if len(fstype) > 0 {
|
||||
mountArgs = append(mountArgs, "-t", fstype)
|
||||
mountArgsLogStr += strings.Join(mountArgs, " ")
|
||||
}
|
||||
if len(options) > 0 || len(sensitiveOptions) > 0 {
|
||||
combinedOptions := []string{}
|
||||
combinedOptions = append(combinedOptions, options...)
|
||||
combinedOptions = append(combinedOptions, sensitiveOptions...)
|
||||
mountArgs = append(mountArgs, "-o", strings.Join(combinedOptions, ","))
|
||||
// exclude sensitiveOptions from log string
|
||||
mountArgsLogStr += " -o " + sanitizedOptionsForLogging(options, sensitiveOptions)
|
||||
}
|
||||
if len(source) > 0 {
|
||||
mountArgs = append(mountArgs, source)
|
||||
mountArgsLogStr += " " + source
|
||||
}
|
||||
mountArgs = append(mountArgs, target)
|
||||
mountArgsLogStr += " " + target
|
||||
|
||||
return mountArgs, mountArgsLogStr
|
||||
}
|
||||
|
||||
// AddSystemdScope adds "system-run --scope" to given command line
|
||||
// If args contains sensitive material, use AddSystemdScopeSensitive to construct
|
||||
// a safe to log string.
|
||||
func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) {
|
||||
descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName)
|
||||
systemdRunArgs := []string{descriptionArg, "--scope", "--", command}
|
||||
return systemdRunPath, append(systemdRunArgs, args...)
|
||||
}
|
||||
|
||||
// AddSystemdScopeSensitive adds "system-run --scope" to given command line
|
||||
// It also accepts takes a sanitized string containing mount arguments, mountArgsLogStr,
|
||||
// and returns the string appended to the systemd command for logging.
|
||||
func AddSystemdScopeSensitive(systemdRunPath, mountName, command string, args []string, mountArgsLogStr string) (string, []string, string) {
|
||||
descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName)
|
||||
systemdRunArgs := []string{descriptionArg, "--scope", "--", command}
|
||||
return systemdRunPath, append(systemdRunArgs, args...), strings.Join(systemdRunArgs, " ") + " " + mountArgsLogStr
|
||||
}
|
||||
|
||||
// Unmount unmounts the target.
|
||||
// If the mounter has safe "not mounted" behavior, no error will be returned when the target is not a mount point.
|
||||
func (mounter *Mounter) Unmount(target string) error {
|
||||
klog.V(4).Infof("Unmounting %s", target)
|
||||
command := exec.Command("umount", target)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return checkUmountError(target, command, output, err, mounter.withSafeNotMountedBehavior)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmountWithForce unmounts given target but will retry unmounting with force option
|
||||
// after given timeout.
|
||||
func (mounter *Mounter) UnmountWithForce(target string, umountTimeout time.Duration) error {
|
||||
err := tryUnmount(target, mounter.withSafeNotMountedBehavior, umountTimeout)
|
||||
if err != nil {
|
||||
if err == context.DeadlineExceeded {
|
||||
klog.V(2).Infof("Timed out waiting for unmount of %s, trying with -f", target)
|
||||
err = forceUmount(target, mounter.withSafeNotMountedBehavior)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns a list of all mounted filesystems.
|
||||
func (*Mounter) List() ([]MountPoint, error) {
|
||||
return ListProcMounts(procMountsPath)
|
||||
}
|
||||
|
||||
// IsLikelyNotMountPoint determines if a directory is not a mountpoint.
|
||||
// It is fast but not necessarily ALWAYS correct. If the path is in fact
|
||||
// a bind mount from one part of a mount to another it will not be detected.
|
||||
// It also can not distinguish between mountpoints and symbolic links.
|
||||
// mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b")
|
||||
// will return true. When in fact /tmp/b is a mount point. If this situation
|
||||
// is of interest to you, don't use this function...
|
||||
func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) {
|
||||
stat, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/")))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
// If the directory has a different device as parent, then it is a mountpoint.
|
||||
if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CanSafelySkipMountPointCheck relies on the detected behavior of umount when given a target that is not a mount point.
|
||||
func (mounter *Mounter) CanSafelySkipMountPointCheck() bool {
|
||||
return mounter.withSafeNotMountedBehavior
|
||||
}
|
||||
|
||||
// GetMountRefs finds all mount references to pathname, returns a
|
||||
// list of paths. Path could be a mountpoint or a normal
|
||||
// directory (for bind mount).
|
||||
func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) {
|
||||
pathExists, pathErr := PathExists(pathname)
|
||||
if !pathExists {
|
||||
return []string{}, nil
|
||||
} else if IsCorruptedMnt(pathErr) {
|
||||
klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname)
|
||||
return []string{}, nil
|
||||
} else if pathErr != nil {
|
||||
return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr)
|
||||
}
|
||||
realpath, err := filepath.EvalSymlinks(pathname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return SearchMountPoints(realpath, procMountInfoPath)
|
||||
}
|
||||
|
||||
// checkAndRepairFileSystem checks and repairs filesystems using command fsck.
|
||||
func (mounter *SafeFormatAndMount) checkAndRepairFilesystem(source string) error {
|
||||
klog.V(4).Infof("Checking for issues with fsck on disk: %s", source)
|
||||
args := []string{"-a", source}
|
||||
out, err := mounter.Exec.Command("fsck", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
ee, isExitError := err.(utilexec.ExitError)
|
||||
switch {
|
||||
case err == utilexec.ErrExecutableNotFound:
|
||||
klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.")
|
||||
case isExitError && ee.ExitStatus() == fsckErrorsCorrected:
|
||||
klog.Infof("Device %s has errors which were corrected by fsck.", source)
|
||||
case isExitError && ee.ExitStatus() == fsckErrorsUncorrected:
|
||||
return NewMountError(HasFilesystemErrors, "'fsck' found errors on device %s but could not correct them: %s", source, string(out))
|
||||
case isExitError && ee.ExitStatus() > fsckErrorsUncorrected:
|
||||
klog.Infof("`fsck` error %s", string(out))
|
||||
default:
|
||||
klog.Warningf("fsck on device %s failed with error %v, output: %v", source, err, string(out))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatAndMount uses unix utils to format and mount the given disk
|
||||
func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error {
|
||||
readOnly := false
|
||||
for _, option := range options {
|
||||
if option == "ro" {
|
||||
readOnly = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !readOnly {
|
||||
// Check sensitiveOptions for ro
|
||||
for _, option := range sensitiveOptions {
|
||||
if option == "ro" {
|
||||
readOnly = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
options = append(options, "defaults")
|
||||
mountErrorValue := UnknownMountError
|
||||
|
||||
// Check if the disk is already formatted
|
||||
existingFormat, err := mounter.GetDiskFormat(source)
|
||||
if err != nil {
|
||||
return NewMountError(GetDiskFormatFailed, "failed to get disk format of disk %s: %v", source, err)
|
||||
}
|
||||
|
||||
// Use 'ext4' as the default
|
||||
if len(fstype) == 0 {
|
||||
fstype = "ext4"
|
||||
}
|
||||
|
||||
if existingFormat == "" {
|
||||
// Do not attempt to format the disk if mounting as readonly, return an error to reflect this.
|
||||
if readOnly {
|
||||
return NewMountError(UnformattedReadOnly, "cannot mount unformatted disk %s as we are manipulating it in read-only mode", source)
|
||||
}
|
||||
|
||||
// Disk is unformatted so format it.
|
||||
args := []string{source}
|
||||
if fstype == "ext4" || fstype == "ext3" {
|
||||
args = []string{
|
||||
"-F", // Force flag
|
||||
"-m0", // Zero blocks reserved for super-user
|
||||
source,
|
||||
}
|
||||
} else if fstype == "xfs" {
|
||||
args = []string{
|
||||
"-f", // force flag
|
||||
source,
|
||||
}
|
||||
}
|
||||
args = append(formatOptions, args...)
|
||||
|
||||
klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args)
|
||||
|
||||
output, err := mounter.format(fstype, args)
|
||||
if err != nil {
|
||||
// Do not log sensitiveOptions only options
|
||||
sensitiveOptionsLog := sanitizedOptionsForLogging(options, sensitiveOptions)
|
||||
detailedErr := fmt.Sprintf("format of disk %q failed: type:(%q) target:(%q) options:(%q) errcode:(%v) output:(%v) ", source, fstype, target, sensitiveOptionsLog, err, string(output))
|
||||
klog.Error(detailedErr)
|
||||
return NewMountError(FormatFailed, detailedErr)
|
||||
}
|
||||
|
||||
klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target)
|
||||
} else {
|
||||
if fstype != existingFormat {
|
||||
// Verify that the disk is formatted with filesystem type we are expecting
|
||||
mountErrorValue = FilesystemMismatch
|
||||
klog.Warningf("Configured to mount disk %s as %s but current format is %s, things might break", source, existingFormat, fstype)
|
||||
}
|
||||
|
||||
if !readOnly {
|
||||
// Run check tools on the disk to fix repairable issues, only do this for formatted volumes requested as rw.
|
||||
err := mounter.checkAndRepairFilesystem(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the disk
|
||||
klog.V(4).Infof("Attempting to mount disk %s in %s format at %s", source, fstype, target)
|
||||
if err := mounter.MountSensitive(source, target, fstype, options, sensitiveOptions); err != nil {
|
||||
return NewMountError(mountErrorValue, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mounter *SafeFormatAndMount) format(fstype string, args []string) ([]byte, error) {
|
||||
if mounter.formatSem != nil {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
mounter.formatSem <- struct{}{}
|
||||
|
||||
go func() {
|
||||
defer func() { <-mounter.formatSem }()
|
||||
|
||||
timeout := time.NewTimer(mounter.formatTimeout)
|
||||
defer timeout.Stop()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-timeout.C:
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return mounter.Exec.Command("mkfs."+fstype, args...).CombinedOutput()
|
||||
}
|
||||
|
||||
func getDiskFormat(exec utilexec.Interface, disk string) (string, error) {
|
||||
args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk}
|
||||
klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args)
|
||||
dataOut, err := exec.Command("blkid", args...).CombinedOutput()
|
||||
output := string(dataOut)
|
||||
klog.V(4).Infof("Output: %q", output)
|
||||
|
||||
if err != nil {
|
||||
if exit, ok := err.(utilexec.ExitError); ok {
|
||||
if exit.ExitStatus() == 2 {
|
||||
// Disk device is unformatted.
|
||||
// For `blkid`, if the specified token (TYPE/PTTYPE, etc) was
|
||||
// not found, or no (specified) devices could be identified, an
|
||||
// exit code of 2 is returned.
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
var fstype, pttype string
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, l := range lines {
|
||||
if len(l) <= 0 {
|
||||
// Ignore empty line.
|
||||
continue
|
||||
}
|
||||
cs := strings.Split(l, "=")
|
||||
if len(cs) != 2 {
|
||||
return "", fmt.Errorf("blkid returns invalid output: %s", output)
|
||||
}
|
||||
// TYPE is filesystem type, and PTTYPE is partition table type, according
|
||||
// to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/.
|
||||
if cs[0] == "TYPE" {
|
||||
fstype = cs[1]
|
||||
} else if cs[0] == "PTTYPE" {
|
||||
pttype = cs[1]
|
||||
}
|
||||
}
|
||||
|
||||
if len(pttype) > 0 {
|
||||
klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype)
|
||||
// Returns a special non-empty string as filesystem type, then kubelet
|
||||
// will not format it.
|
||||
return "unknown data, probably partitions", nil
|
||||
}
|
||||
|
||||
return fstype, nil
|
||||
}
|
||||
|
||||
// GetDiskFormat uses 'blkid' to see if the given disk is unformatted
|
||||
func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) {
|
||||
return getDiskFormat(mounter.Exec, disk)
|
||||
}
|
||||
|
||||
// ListProcMounts is shared with NsEnterMounter
|
||||
func ListProcMounts(mountFilePath string) ([]MountPoint, error) {
|
||||
content, err := readMountInfo(mountFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseProcMounts(content)
|
||||
}
|
||||
|
||||
func parseProcMounts(content []byte) ([]MountPoint, error) {
|
||||
out := []MountPoint{}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
// the last split() item is empty string following the last \n
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) != expectedNumFieldsPerLine {
|
||||
// Do not log line in case it contains sensitive Mount options
|
||||
return nil, fmt.Errorf("wrong number of fields (expected %d, got %d)", expectedNumFieldsPerLine, len(fields))
|
||||
}
|
||||
|
||||
mp := MountPoint{
|
||||
Device: fields[0],
|
||||
Path: fields[1],
|
||||
Type: fields[2],
|
||||
Opts: strings.Split(fields[3], ","),
|
||||
}
|
||||
|
||||
freq, err := strconv.Atoi(fields[4])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mp.Freq = freq
|
||||
|
||||
pass, err := strconv.Atoi(fields[5])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mp.Pass = pass
|
||||
|
||||
out = append(out, mp)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SearchMountPoints finds all mount references to the source, returns a list of
|
||||
// mountpoints.
|
||||
// The source can be a mount point or a normal directory (bind mount). We
|
||||
// didn't support device because there is no use case by now.
|
||||
// Some filesystems may share a source name, e.g. tmpfs. And for bind mounting,
|
||||
// it's possible to mount a non-root path of a filesystem, so we need to use
|
||||
// root path and major:minor to represent mount source uniquely.
|
||||
// This implementation is shared between Linux and NsEnterMounter
|
||||
func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) {
|
||||
mis, err := ParseMountInfo(mountInfoPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountID := 0
|
||||
rootPath := ""
|
||||
major := -1
|
||||
minor := -1
|
||||
|
||||
// Finding the underlying root path and major:minor if possible.
|
||||
// We need search in backward order because it's possible for later mounts
|
||||
// to overlap earlier mounts.
|
||||
for i := len(mis) - 1; i >= 0; i-- {
|
||||
if hostSource == mis[i].MountPoint || PathWithinBase(hostSource, mis[i].MountPoint) {
|
||||
// If it's a mount point or path under a mount point.
|
||||
mountID = mis[i].ID
|
||||
rootPath = filepath.Join(mis[i].Root, strings.TrimPrefix(hostSource, mis[i].MountPoint))
|
||||
major = mis[i].Major
|
||||
minor = mis[i].Minor
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if rootPath == "" || major == -1 || minor == -1 {
|
||||
return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource)
|
||||
}
|
||||
|
||||
var refs []string
|
||||
for i := range mis {
|
||||
if mis[i].ID == mountID {
|
||||
// Ignore mount entry for mount source itself.
|
||||
continue
|
||||
}
|
||||
if mis[i].Root == rootPath && mis[i].Major == major && mis[i].Minor == minor {
|
||||
refs = append(refs, mis[i].MountPoint)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
// IsMountPoint determines if a file is a mountpoint.
|
||||
// It first detects bind & any other mountpoints using
|
||||
// MountedFast function. If the MountedFast function returns
|
||||
// sure as true and err as nil, then a mountpoint is detected
|
||||
// successfully. When an error is returned by MountedFast, the
|
||||
// following is true:
|
||||
// 1. All errors are returned with IsMountPoint as false
|
||||
// except os.IsPermission.
|
||||
// 2. When os.IsPermission is returned by MountedFast, List()
|
||||
// is called to confirm if the given file is a mountpoint are not.
|
||||
//
|
||||
// os.ErrNotExist should always be returned if a file does not exist
|
||||
// as callers have in past relied on this error and not fallback.
|
||||
//
|
||||
// When MountedFast returns sure as false and err as nil (eg: in
|
||||
// case of bindmounts on kernel version 5.10- ); mounter.List()
|
||||
// endpoint is called to enumerate all the mountpoints and check if
|
||||
// it is mountpoint match or not.
|
||||
func (mounter *Mounter) IsMountPoint(file string) (bool, error) {
|
||||
isMnt, sure, isMntErr := mountinfo.MountedFast(file)
|
||||
if sure && isMntErr == nil {
|
||||
return isMnt, nil
|
||||
}
|
||||
if isMntErr != nil {
|
||||
if errors.Is(isMntErr, fs.ErrNotExist) {
|
||||
return false, fs.ErrNotExist
|
||||
}
|
||||
// We were not allowed to do the simple stat() check, e.g. on NFS with
|
||||
// root_squash. Fall back to /proc/mounts check below when
|
||||
// fs.ErrPermission is returned.
|
||||
if !errors.Is(isMntErr, fs.ErrPermission) {
|
||||
return false, isMntErr
|
||||
}
|
||||
}
|
||||
// Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts.
|
||||
resolvedFile, err := filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return false, fs.ErrNotExist
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// check all mountpoints since MountedFast is not sure.
|
||||
// is not reliable for some mountpoint types.
|
||||
mountPoints, mountPointsErr := mounter.List()
|
||||
if mountPointsErr != nil {
|
||||
return false, mountPointsErr
|
||||
}
|
||||
for _, mp := range mountPoints {
|
||||
if isMountPointMatch(mp, resolvedFile) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// tryUnmount calls plain "umount" and waits for unmountTimeout for it to finish.
|
||||
func tryUnmount(target string, withSafeNotMountedBehavior bool, unmountTimeout time.Duration) error {
|
||||
klog.V(4).Infof("Unmounting %s", target)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), unmountTimeout)
|
||||
defer cancel()
|
||||
|
||||
command := exec.CommandContext(ctx, "umount", target)
|
||||
output, err := command.CombinedOutput()
|
||||
|
||||
// CombinedOutput() does not return DeadlineExceeded, make sure it's
|
||||
// propagated on timeout.
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return checkUmountError(target, command, output, err, withSafeNotMountedBehavior)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceUmount(target string, withSafeNotMountedBehavior bool) error {
|
||||
command := exec.Command("umount", "-f", target)
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return checkUmountError(target, command, output, err, withSafeNotMountedBehavior)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkUmountError checks a result of umount command and determine a return value.
|
||||
func checkUmountError(target string, command *exec.Cmd, output []byte, err error, withSafeNotMountedBehavior bool) error {
|
||||
if err.Error() == errNoChildProcesses {
|
||||
if command.ProcessState.Success() {
|
||||
// We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753).
|
||||
return nil
|
||||
}
|
||||
// Rewrite err with the actual exit error of the process.
|
||||
err = &exec.ExitError{ProcessState: command.ProcessState}
|
||||
}
|
||||
if withSafeNotMountedBehavior && strings.Contains(string(output), errNotMounted) {
|
||||
klog.V(4).Infof("ignoring 'not mounted' error for %s", target)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output))
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"git.ruekov.eu/ruakij/webdav-csi-driver/pkg/webdav/mount"
|
||||
)
|
||||
|
||||
type NodeServer struct {
|
||||
csi.UnimplementedNodeServer
|
||||
Driver *Driver
|
||||
mounter mount.Interface
|
||||
}
|
||||
|
||||
func NewNodeServer(d *Driver, mounter mount.Interface) *NodeServer {
|
||||
return &NodeServer{
|
||||
Driver: d,
|
||||
mounter: mounter,
|
||||
}
|
||||
}
|
||||
|
||||
// NodePublishVolume implements csi.NodeServer.
|
||||
func (n *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
|
||||
volCap := req.GetVolumeCapability()
|
||||
if volCap == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume capability missing in request")
|
||||
}
|
||||
volumeID := req.GetVolumeId()
|
||||
if len(volumeID) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
targetPath := req.GetTargetPath()
|
||||
if len(targetPath) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Target path not provided")
|
||||
}
|
||||
|
||||
mountOptions := volCap.GetMount().GetMountFlags()
|
||||
if req.GetReadonly() {
|
||||
mountOptions = append(mountOptions, "ro")
|
||||
}
|
||||
|
||||
address, subDir, err := ParseVolumeId(volumeID)
|
||||
if err != nil {
|
||||
// An invalid ID should be treated as doesn't exist
|
||||
klog.Warningf("failed to parse volume for volume id %v deletion: %v", volumeID, err)
|
||||
return &csi.NodePublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(n.Driver.mountPermissions)); err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
notMnt = true
|
||||
} else {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
}
|
||||
if !notMnt {
|
||||
return &csi.NodePublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
sourcePath := strings.Join([]string{address, subDir}, "/")
|
||||
stdin := []string{req.GetSecrets()[secretUsernameKey], req.GetSecrets()[secretPasswordKey]}
|
||||
klog.V(2).Infof("NodePublishVolume: volumeID(%v) source(%s) targetPath(%s) mountflags(%v)", volumeID, sourcePath, targetPath, mountOptions)
|
||||
err = n.mounter.MountSensitiveWithStdin(sourcePath, targetPath, fstype, mountOptions, nil, stdin)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), "invalid argument") {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
return &csi.NodePublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
// NodeUnpublishVolume implements csi.NodeServer.
|
||||
func (n *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
|
||||
volumeID := req.GetVolumeId()
|
||||
if len(volumeID) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Volume ID missing in request")
|
||||
}
|
||||
targetPath := req.GetTargetPath()
|
||||
if len(targetPath) == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "Target path missing in request")
|
||||
}
|
||||
|
||||
notMnt, err := n.mounter.IsLikelyNotMountPoint(targetPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, status.Error(codes.NotFound, "Targetpath not found")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
if notMnt {
|
||||
return &csi.NodeUnpublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
klog.V(2).Infof("NodeUnpublishVolume: unmounting volume %s on %s", volumeID, targetPath)
|
||||
err = n.mounter.Unmount(targetPath)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to unmount target %q: %v", targetPath, err)
|
||||
}
|
||||
|
||||
klog.V(2).Infof("NodeUnpublishVolume: unmount volume %s on %s successfully", volumeID, targetPath)
|
||||
return &csi.NodeUnpublishVolumeResponse{}, nil
|
||||
}
|
||||
|
||||
// NodeGetInfo implements csi.NodeServer.
|
||||
func (n *NodeServer) NodeGetInfo(context.Context, *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
|
||||
return &csi.NodeGetInfoResponse{NodeId: n.Driver.nodeID}, nil
|
||||
}
|
||||
|
||||
// NodeGetCapabilities implements csi.NodeServer.
|
||||
func (n *NodeServer) NodeGetCapabilities(context.Context, *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
|
||||
return &csi.NodeGetCapabilitiesResponse{
|
||||
Capabilities: n.Driver.nscap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NodeExpandVolume implements csi.NodeServer.
|
||||
func (*NodeServer) NodeExpandVolume(context.Context, *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// NodeGetVolumeStats implements csi.NodeServer.
|
||||
func (*NodeServer) NodeGetVolumeStats(context.Context, *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// NodeStageVolume implements csi.NodeServer.
|
||||
func (*NodeServer) NodeStageVolume(context.Context, *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
||||
|
||||
// NodeUnstageVolume implements csi.NodeServer.
|
||||
func (*NodeServer) NodeUnstageVolume(context.Context, *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "")
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// Defines Non blocking GRPC server interfaces
|
||||
type NonBlockingGRPCServer interface {
|
||||
// Start services at the endpoint
|
||||
Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer)
|
||||
// Waits for the service to stop
|
||||
Wait()
|
||||
// Stops the service gracefully
|
||||
Stop()
|
||||
// Stops the service forcefully
|
||||
ForceStop()
|
||||
}
|
||||
|
||||
func NewNonBlockingGRPCServer() NonBlockingGRPCServer {
|
||||
return &nonBlockingGRPCServer{}
|
||||
}
|
||||
|
||||
// NonBlocking server
|
||||
type nonBlockingGRPCServer struct {
|
||||
wg sync.WaitGroup
|
||||
server *grpc.Server
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Start(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
|
||||
s.wg.Add(1)
|
||||
go s.serve(endpoint, ids, cs, ns)
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Wait() {
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) Stop() {
|
||||
s.server.GracefulStop()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) ForceStop() {
|
||||
s.server.Stop()
|
||||
}
|
||||
|
||||
func (s *nonBlockingGRPCServer) serve(endpoint string, ids csi.IdentityServer, cs csi.ControllerServer, ns csi.NodeServer) {
|
||||
proto, addr, err := ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
klog.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if proto == "unix" {
|
||||
addr = "/" + addr
|
||||
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
|
||||
klog.Fatalf("Failed to remove %s, error: %s", addr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
listener, err := net.Listen(proto, addr)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to listen: %v", err)
|
||||
}
|
||||
|
||||
opts := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(logGRPC),
|
||||
}
|
||||
server := grpc.NewServer(opts...)
|
||||
s.server = server
|
||||
|
||||
if ids != nil {
|
||||
csi.RegisterIdentityServer(server, ids)
|
||||
}
|
||||
if cs != nil {
|
||||
csi.RegisterControllerServer(server, cs)
|
||||
}
|
||||
if ns != nil {
|
||||
csi.RegisterNodeServer(server, ns)
|
||||
}
|
||||
|
||||
klog.Infof("Listening for connections on address: %#v", listener.Addr())
|
||||
|
||||
err = server.Serve(listener)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to serve grpc server: %v", err)
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/container-storage-interface/spec/lib/go/csi"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func ParseEndpoint(ep string) (string, string, error) {
|
||||
if strings.HasPrefix(strings.ToLower(ep), "unix://") || strings.HasPrefix(strings.ToLower(ep), "tcp://") {
|
||||
s := strings.SplitN(ep, "://", 2)
|
||||
if s[1] != "" {
|
||||
return s[0], s[1], nil
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("invalid endpoint: %v", ep)
|
||||
}
|
||||
|
||||
func getLogLevel(method string) int32 {
|
||||
if method == "/csi.v1.Identity/Probe" ||
|
||||
method == "/csi.v1.Node/NodeGetCapabilities" ||
|
||||
method == "/csi.v1.Node/NodeGetVolumeStats" {
|
||||
return 8
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func logGRPC(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
level := klog.Level(getLogLevel(info.FullMethod))
|
||||
klog.V(level).Infof("GRPC call: %s", info.FullMethod)
|
||||
klog.V(level).Infof("GRPC request: %s", req)
|
||||
|
||||
resp, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
klog.Errorf("GRPC error: %v", err)
|
||||
} else {
|
||||
klog.V(level).Infof("GRPC response: %s", resp)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func NewControllerServiceCapability(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability {
|
||||
return &csi.ControllerServiceCapability{
|
||||
Type: &csi.ControllerServiceCapability_Rpc{
|
||||
Rpc: &csi.ControllerServiceCapability_RPC{
|
||||
Type: cap,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewNodeServiceCapability(cap csi.NodeServiceCapability_RPC_Type) *csi.NodeServiceCapability {
|
||||
return &csi.NodeServiceCapability{
|
||||
Type: &csi.NodeServiceCapability_Rpc{
|
||||
Rpc: &csi.NodeServiceCapability_RPC{
|
||||
Type: cap,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func MakeVolumeId(webdavSharePath, volumeName string) string {
|
||||
return fmt.Sprintf("%s#%s", webdavSharePath, volumeName)
|
||||
}
|
||||
|
||||
func ParseVolumeId(volumeId string) (webdavSharePath, subDir string, err error) {
|
||||
arr := strings.Split(volumeId, "#")
|
||||
if len(arr) < 2 {
|
||||
return "", "", errors.New("invalid volumeId")
|
||||
}
|
||||
return arr[0], arr[1], nil
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2023.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// These are set during build time via -ldflags
|
||||
var (
|
||||
driverVersion = "N/A"
|
||||
gitCommit = "N/A"
|
||||
buildDate = "N/A"
|
||||
)
|
||||
|
||||
// VersionInfo holds the version information of the driver
|
||||
type VersionInfo struct {
|
||||
DriverName string `json:"Driver Name"`
|
||||
DriverVersion string `json:"Driver Version"`
|
||||
GitCommit string `json:"Git Commit"`
|
||||
BuildDate string `json:"Build Date"`
|
||||
GoVersion string `json:"Go Version"`
|
||||
Compiler string `json:"Compiler"`
|
||||
Platform string `json:"Platform"`
|
||||
}
|
||||
|
||||
// GetVersion returns the version information of the driver
|
||||
func GetVersion(driverName string) VersionInfo {
|
||||
return VersionInfo{
|
||||
DriverName: driverName,
|
||||
DriverVersion: driverVersion,
|
||||
GitCommit: gitCommit,
|
||||
BuildDate: buildDate,
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
}
|
||||
|
||||
// GetVersionYAML returns the version information of the driver
|
||||
func GetVersionYAML(driverName string) (string, error) {
|
||||
info := GetVersion(driverName)
|
||||
marshalled, err := yaml.Marshal(&info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(marshalled)), nil
|
||||
}
|
@ -0,0 +1 @@
|
||||
kind create cluster --config=kind.yaml
|
@ -0,0 +1,18 @@
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
- role: control-plane
|
||||
image: kindest/node:v1.29.0
|
||||
extraMounts:
|
||||
- hostPath: /root/workspace/csi-driver-webdav/test/csi
|
||||
containerPath: /csi
|
||||
networking:
|
||||
apiServerPort: 6443
|
||||
podSubnet: 172.16.0.0/16
|
||||
serviceSubnet: 172.19.0.0/16
|
||||
containerdConfigPatches:
|
||||
- |-
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
|
||||
endpoint = ["https://hub-mirror.c.163.com"]
|
||||
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5000"]
|
||||
endpoint = ["http://registry:5000"]
|
Loading…
Reference in New Issue