From 6f6fd030c638d996e82d4cf990a48336dcadf7e7 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:14:34 +0100 Subject: [PATCH 1/7] feat: use nsenter and systemd-run to mount --- go.mod | 5 --- go.sum | 5 --- pkg/rclone/nodeserver.go | 92 ++++++++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index f27cc4f3..fa02cc00 100644 --- a/go.mod +++ b/go.mod @@ -26,8 +26,6 @@ require ( ) require ( - github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect - github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/beorn7/perks v1.0.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -42,7 +40,6 @@ require ( github.com/imdario/mergo v0.3.7 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/json-iterator/go v1.1.10 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect github.com/kubernetes-csi/csi-lib-utils v0.3.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect @@ -52,7 +49,6 @@ require ( github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect github.com/prometheus/common v0.4.0 // indirect github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 // indirect - github.com/sirupsen/logrus v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect @@ -64,7 +60,6 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 99033f00..6c1f273d 100644 --- a/go.sum +++ b/go.sum @@ -37,9 +37,7 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -212,7 +210,6 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -300,7 +297,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -570,7 +566,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/rclone/nodeserver.go b/pkg/rclone/nodeserver.go index ef035016..8ab439c8 100644 --- a/pkg/rclone/nodeserver.go +++ b/pkg/rclone/nodeserver.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "os" + "os/exec" "strings" "time" @@ -48,6 +49,43 @@ func (ns *nodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstag return nil, status.Errorf(codes.Unimplemented, "method NodeUnstageVolume not implemented") } +func configToEnvMap(configData string) (map[string]string, error) { + envVars := make(map[string]string) + + // Parse the string data + cfg, err := ini.Load([]byte(configData)) + if err != nil { + return nil, fmt.Errorf("failed to parse config: %w", err) + } + + for _, section := range cfg.Sections() { + remoteName := section.Name() + + // Skip the internal INI "DEFAULT" section + // I.e. the section without a heading that is not in any heading + // NOTE: This will clash with any section i.e. remote that is named "[DEFAULT]" + if remoteName == "DEFAULT" { + continue + } + + // rclone convention: RCLONE_CONFIG_REMOTENAME_KEYNAME + // 1. Replace hyphens/spaces with underscores + // 2. Convert to uppercase + envRemote := strings.ToUpper(strings.ReplaceAll(remoteName, "-", "_")) + envRemote = strings.ReplaceAll(envRemote, " ", "_") + + for _, key := range section.Keys() { + envKey := strings.ToUpper(strings.ReplaceAll(key.Name(), "-", "_")) + envKey = strings.ReplaceAll(envKey, " ", "_") + + fullKey := fmt.Sprintf("RCLONE_CONFIG_%s_%s", envRemote, envKey) + envVars[fullKey] = key.String() + } + } + + return envVars, nil +} + // Mounting Volume (Actual Mounting) func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) { if err := validatePublishVolumeRequest(req); err != nil { @@ -55,9 +93,9 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis } targetPath := req.GetTargetPath() - volumeId := req.GetVolumeId() + //volumeId := req.GetVolumeId() volumeContext := req.GetVolumeContext() - readOnly := req.GetReadonly() + //readOnly := req.GetReadonly() secretName, foundSecret := volumeContext["secretName"] secretNamespace, foundSecretNamespace := volumeContext["secretNamespace"] // For backwards compatibility - prior to the change in #20 this field held the namespace @@ -126,25 +164,43 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis } } - rcloneVol := &RcloneVolume{ - ID: volumeId, - Remote: remote, - RemotePath: remotePath, + // rcloneVol := &RcloneVolume{ + // ID: volumeId, + // Remote: remote, + // RemotePath: remotePath, + // } + + envVars, err := configToEnvMap(configData) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + // 2. Build the systemd-run command with --setenv + args := []string{ + "--mount=/host/proc/1/ns/mnt", + "--", + "/host/usr/bin/systemd-run", + "--scope", + } + + // Inject rclon config as env vars + for k, v := range envVars { + args = append(args, fmt.Sprintf("--setenv=%s=%s", k, v)) } - err = ns.RcloneOps.Mount(ctx, rcloneVol, targetPath, configData, readOnly, flags) + + args = append(args, + "/opt/rclone-csi/bin/rclone", "mount", + remote+":"+remotePath, req.GetTargetPath(), + "--daemon", + "--allow-other", + ) + + cmd := exec.Command("/host/usr/bin/nsenter", args...) + err = cmd.Run() 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 nil, status.Error(codes.InvalidArgument, err.Error()) } - // err = ns.WaitForMountAvailable(targetPath) - // if err != nil { - // return nil, status.Error(codes.Internal, err.Error()) - // } + return &csi.NodePublishVolumeResponse{}, nil } From 49d9a2762d93c308fec8e7c680441d21f32fe987 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:16:56 +0100 Subject: [PATCH 2/7] chore: add init for copying over rclone --- pkg/rclone/driver.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/rclone/driver.go b/pkg/rclone/driver.go index 60fd38ba..9e12bac9 100644 --- a/pkg/rclone/driver.go +++ b/pkg/rclone/driver.go @@ -130,6 +130,22 @@ func (d *Driver) WithControllerServer(cs *controllerServer) *Driver { } func (d *Driver) Run() error { + init := func() { + // Path inside your container image + src := "/usr/local/bin/rclone" + // Path on the host (accessed via a volume mount) + dst := "/host/opt/rclone-csi/bin/rclone" + + // Create directory if it doesn't exist + os.MkdirAll("/host/opt/rclone-csi/bin", 0755) + + // Copy the binary from the image to the host path + // This ensures the host can "see" the rclone binary + // even if the pod is currently restarting. + data, _ := os.ReadFile(src) + os.WriteFile(dst, data, 0755) + } + init() s := csicommon.NewNonBlockingGRPCServer() s.Start( d.endpoint, From de12453a76cdbe7b0b65f8884678a2bfd81ab34c Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:25:37 +0100 Subject: [PATCH 3/7] chore: modify helm chart --- .../templates/csi-nodeplugin-rclone.yaml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml b/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml index 7615eba6..fa304666 100644 --- a/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml +++ b/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml @@ -143,6 +143,20 @@ spec: name: pods-mount-dir - mountPath: /var/lib/rclone name: cache-dir + # Host Proc for nsenter + - name: host-proc + mountPath: /host/proc + readOnly: true + # Host Binaries for systemd-run and nsenter + - name: host-usr-bin + mountPath: /host/usr/bin + readOnly: true + # FUSE Device + - name: dev-fuse + mountPath: /dev/fuse + # The Binary Bridge for rclone + - name: bin-bridge + mountPath: /opt/rclone-csi/bin {{- with .Values.csiNodepluginRclone.nodeSelector }} nodeSelector: {{ toYaml . | nindent 8 }} @@ -170,3 +184,19 @@ spec: name: registration-dir - name: cache-dir emptyDir: {} + - name: host-proc + hostPath: + path: /proc + type: Directory + - name: host-usr-bin + hostPath: + path: /usr/bin + type: Directory + - name: dev-fuse + hostPath: + path: /dev/fuse + type: CharDevice + - name: bin-bridge + hostPath: + path: /opt/rclone-csi/bin + type: DirectoryOrCreate From 53143c69280be07619fd1660ea0a3d411f1a12f7 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:36:41 +0100 Subject: [PATCH 4/7] squashme: minor fix --- pkg/rclone/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rclone/driver.go b/pkg/rclone/driver.go index 9e12bac9..575d82d3 100644 --- a/pkg/rclone/driver.go +++ b/pkg/rclone/driver.go @@ -132,7 +132,7 @@ func (d *Driver) WithControllerServer(cs *controllerServer) *Driver { func (d *Driver) Run() error { init := func() { // Path inside your container image - src := "/usr/local/bin/rclone" + src := "/usr/bin/rclone" // Path on the host (accessed via a volume mount) dst := "/host/opt/rclone-csi/bin/rclone" From e1c7b690e8bca207983a9781064cbf7d07bfe0f2 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:44:29 +0100 Subject: [PATCH 5/7] chore: add test values and pod --- test-pod.yaml | 42 ++++++++++++++++++++++++++++++++++++++++++ test-values.yaml | 1 + 2 files changed, 43 insertions(+) create mode 100644 test-pod.yaml create mode 100644 test-values.yaml diff --git a/test-pod.yaml b/test-pod.yaml new file mode 100644 index 00000000..5b60c91d --- /dev/null +++ b/test-pod.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: csi-rclone-example + annotations: + csi-rclone.dev/secretName: csi-rclone-example-secret +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi + storageClassName: tasko-csi-rclone +--- +apiVersion: v1 +kind: Secret +metadata: + name: csi-rclone-example-secret +type: Opaque +stringData: + remote: giab + remotePath: giab/ + configData: | + [giab] + type = s3 + provider = AWS +--- +apiVersion: v1 +kind: Pod +metadata: + name: test-pod +spec: + containers: + - name: test + image: debian + volumeMounts: + - mountPath: "/giab" + name: task-pv-storage + volumes: + - name: task-pv-storage + persistentVolumeClaim: + claimName: csi-rclone-example diff --git a/test-values.yaml b/test-values.yaml new file mode 100644 index 00000000..c453d115 --- /dev/null +++ b/test-values.yaml @@ -0,0 +1 @@ +storageClassName: tasko-csi-rclone From f5bda49f4192a1dbdfe64b65fbbddfe3d644aeb7 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 01:13:28 +0100 Subject: [PATCH 6/7] squashme: minor fix --- pkg/rclone/driver.go | 4 ++-- pkg/rclone/nodeserver.go | 7 ++++++- test-pod.yaml | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/rclone/driver.go b/pkg/rclone/driver.go index 575d82d3..4cc29319 100644 --- a/pkg/rclone/driver.go +++ b/pkg/rclone/driver.go @@ -134,10 +134,10 @@ func (d *Driver) Run() error { // Path inside your container image src := "/usr/bin/rclone" // Path on the host (accessed via a volume mount) - dst := "/host/opt/rclone-csi/bin/rclone" + dst := "/opt/rclone-csi/bin/rclone" // Create directory if it doesn't exist - os.MkdirAll("/host/opt/rclone-csi/bin", 0755) + os.MkdirAll("/opt/rclone-csi/bin", 0755) // Copy the binary from the image to the host path // This ensures the host can "see" the rclone binary diff --git a/pkg/rclone/nodeserver.go b/pkg/rclone/nodeserver.go index 8ab439c8..a94c8170 100644 --- a/pkg/rclone/nodeserver.go +++ b/pkg/rclone/nodeserver.go @@ -128,6 +128,7 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis return nil, err } else if apierrors.IsNotFound(err) { klog.Warningf("Cannot find saved secrets %s: %s", savedSecretName, err) + savedPvcSecret = nil } remote, remotePath, configData, flags, e := extractFlags(req.GetVolumeContext(), req.GetSecrets(), pvcSecret, savedPvcSecret) @@ -179,7 +180,7 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis args := []string{ "--mount=/host/proc/1/ns/mnt", "--", - "/host/usr/bin/systemd-run", + "systemd-run", "--scope", } @@ -196,8 +197,12 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis ) cmd := exec.Command("/host/usr/bin/nsenter", args...) + cmd.Stderr = os.Stdout + cmd.Stdout = os.Stdout + klog.Infof("Args: %+v", args) err = cmd.Run() if err != nil { + klog.Errorf("Failed to mount with error: %v", err) return nil, status.Error(codes.InvalidArgument, err.Error()) } diff --git a/test-pod.yaml b/test-pod.yaml index 5b60c91d..037d85ed 100644 --- a/test-pod.yaml +++ b/test-pod.yaml @@ -3,19 +3,19 @@ kind: PersistentVolumeClaim metadata: name: csi-rclone-example annotations: - csi-rclone.dev/secretName: csi-rclone-example-secret + csi-rclone.dev/secretName: csi-rclone-test spec: accessModes: - ReadWriteMany resources: requests: storage: 10Gi - storageClassName: tasko-csi-rclone + storageClassName: tasko-csi-rclone-secret-annotation --- apiVersion: v1 kind: Secret metadata: - name: csi-rclone-example-secret + name: csi-rclone-test type: Opaque stringData: remote: giab From 044e6ec3d23db786462cfc1b9822390c5caf1137 Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:02:27 +0100 Subject: [PATCH 7/7] minor fix --- deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml | 2 ++ test-pod.yaml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml b/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml index fa304666..8acab913 100644 --- a/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml +++ b/deploy/csi-rclone/templates/csi-nodeplugin-rclone.yaml @@ -19,6 +19,8 @@ spec: {{- toYaml .Values.csiNodepluginRclone.podAnnotations | nindent 8 }} spec: serviceAccountName: {{ include "chart.fullname" . }}-nodeplugin + hostPID: true + hostIPC: true dnsPolicy: ClusterFirstWithHostNet containers: - name: node-driver-registrar diff --git a/test-pod.yaml b/test-pod.yaml index 037d85ed..4ede033f 100644 --- a/test-pod.yaml +++ b/test-pod.yaml @@ -36,6 +36,8 @@ spec: volumeMounts: - mountPath: "/giab" name: task-pv-storage + command: ["sleep"] + args: ["9999999"] volumes: - name: task-pv-storage persistentVolumeClaim: