Skip to content

Commit 04d7514

Browse files
committed
feat: add remount support
1 parent bb67e1c commit 04d7514

File tree

13 files changed

+386
-116
lines changed

13 files changed

+386
-116
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,49 @@ helm upgrade --install csi-rclone oci://ghcr.io/veloxpack/charts/csi-driver-rclo
130130

131131
</details>
132132

133+
**With Mount Existing (Automatic Remount on Restart):**
134+
135+
Enable automatic remounting of volumes after driver restart. This feature saves mount state to Kubernetes Secrets and automatically remounts volumes when the driver restarts, ensuring volumes remain accessible even after node reboots or driver crashes.
136+
137+
**Use cases:**
138+
- **Node reboots**: Volumes are automatically remounted when the node comes back online
139+
- **Driver restarts**: Maintains volume accessibility during driver updates or crashes
140+
- **High availability**: Ensures persistent volumes remain mounted across driver lifecycle events
141+
142+
**How it works:**
143+
1. Mount state (remote config, mount options) is saved to Kubernetes Secrets when volumes are mounted
144+
2. On driver startup, saved mount states are loaded and volumes are automatically remounted
145+
3. If a volume is already mounted, it will be unmounted and remounted to ensure consistency
146+
147+
**Installation:**
148+
149+
```bash
150+
helm upgrade --install csi-rclone oci://ghcr.io/veloxpack/charts/csi-driver-rclone \
151+
--namespace veloxpack --create-namespace \
152+
--set node.extraArgs.mount-existing=true
153+
```
154+
155+
<details>
156+
<summary>Advanced mount-existing configuration</summary>
157+
158+
The mount state is stored in Kubernetes Secrets in the same namespace as the driver. You can customize the namespace if needed:
159+
160+
```bash
161+
# The mount state manager automatically detects the driver namespace
162+
# Mount states are stored as Secrets with prefix: rclone-mount-state-<hash>
163+
# Each secret contains: volumeId, targetPath, configData, mountParams, etc.
164+
165+
# View saved mount states
166+
kubectl get secrets -n veloxpack -l app.kubernetes.io/name=csi-driver-rclone,app.kubernetes.io/component=mount-state
167+
168+
# Inspect a specific mount state
169+
kubectl get secret rclone-mount-state-<hash> -n veloxpack -o yaml
170+
```
171+
172+
**Note:** When `mount-existing` is enabled, daemon mode is automatically enabled for all mounts to support proper cleanup and remounting.
173+
174+
</details>
175+
133176
**With Remote Control (RC) API:**
134177

135178
Enable the rclone Remote Control API for programmatic control (e.g., VFS cache refresh, stats):

charts/templates/rbac-csi-rclone.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ rules:
8383
- apiGroups: ["storage.k8s.io"]
8484
resources: ["csinodes"]
8585
verbs: ["get", "list", "watch"]
86+
- apiGroups: [""]
87+
resources: ["secrets"]
88+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
8689
---
8790
kind: ClusterRoleBinding
8891
apiVersion: rbac.authorization.k8s.io/v1

charts/values.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ node:
213213

214214
# Additional command-line arguments.
215215
# Example: helm template ./charts --set node.extraArgs.metrics-addr=":5572"
216-
extraArgs: {}
216+
extraArgs:
217+
# Mount existing volume mount points on startup
218+
mount-existing: false
217219

218220
# Cache directory mount configuration
219221
# This allows mounting a host path for the rclone cache directory, useful for:

cmd/rcloneplugin/main.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import (
2929
)
3030

3131
var (
32-
endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI endpoint")
33-
nodeID = flag.String("nodeid", "", "node id")
34-
driverName = flag.String("drivername", rclone.DefaultDriverName, "name of the driver")
32+
endpoint = flag.String("endpoint", "unix://tmp/csi.sock", "CSI endpoint")
33+
nodeID = flag.String("nodeid", "", "node id")
34+
driverName = flag.String("drivername", rclone.DefaultDriverName, "name of the driver")
35+
mountExisting = flag.Bool("mount-existing", false, "mount existing volume mount points on startup")
3536
)
3637

3738
func main() {
@@ -108,9 +109,10 @@ func main() {
108109
}
109110

110111
driverOptions := rclone.DriverOptions{
111-
NodeID: *nodeID,
112-
DriverName: *driverName,
113-
Endpoint: *endpoint,
112+
NodeID: *nodeID,
113+
DriverName: *driverName,
114+
Endpoint: *endpoint,
115+
MountExisting: *mountExisting,
114116
}
115117

116118
driver := rclone.NewDriver(&driverOptions)

deploy/base/rbac-csi-rclone.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,16 @@ roleRef:
5858
kind: ClusterRole
5959
name: rclone-external-provisioner-role
6060
apiGroup: rbac.authorization.k8s.io
61+
---
62+
kind: ClusterRoleBinding
63+
apiVersion: rbac.authorization.k8s.io/v1
64+
metadata:
65+
name: rclone-csi-node-binding
66+
subjects:
67+
- kind: ServiceAccount
68+
name: csi-rclone-node-sa
69+
namespace: system
70+
roleRef:
71+
kind: ClusterRole
72+
name: rclone-external-provisioner-role
73+
apiGroup: rbac.authorization.k8s.io
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: kustomize.config.k8s.io/v1alpha1
2+
kind: Component
3+
4+
patches:
5+
- target:
6+
kind: DaemonSet
7+
name: csi-rclone-node
8+
patch: |-
9+
- op: add
10+
path: /spec/template/spec/containers/2/args/-
11+
value: "--mount-existing"

deploy/example/rclone-secret-storageclass.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ parameters:
1414
# Reference to secret containing sensitive configuration
1515
csi.storage.k8s.io/node-publish-secret-name: "rclone-secret"
1616
csi.storage.k8s.io/node-publish-secret-namespace: "default"
17+
# Optional: Mount options
1718
reclaimPolicy: Delete
1819
volumeBindingMode: Immediate
1920
allowVolumeExpansion: true
21+
mountOptions:
22+
- allow-non-empty
23+
2024
---
2125
# Example Secret for S3 Configuration
2226
apiVersion: v1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
namespace: veloxpack
3+
4+
resources:
5+
- ../../base
6+
7+
components:
8+
- ../../components/mount-existing
9+

pkg/rclone/mount_state.go

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/sha256"
2222
"encoding/json"
2323
"fmt"
24+
"strconv"
2425
"sync"
2526
"time"
2627

@@ -39,7 +40,6 @@ const (
3940
// Kubernetes labels
4041
labelAppName = "app.kubernetes.io/name"
4142
labelComponent = "app.kubernetes.io/component"
42-
labelVolumeID = "rclone.csi.veloxpack.io/volume-id"
4343
labelValueAppName = "csi-driver-rclone"
4444
labelValueComp = "mount-state"
4545

@@ -54,9 +54,7 @@ const (
5454
keyMountParams = "mountParams"
5555
keyMountOptions = "mountOptions"
5656
keyReadOnly = "readonly"
57-
58-
// Default namespace
59-
defaultNamespace = "default"
57+
keyPid = "pid"
6058
)
6159

6260
// MountState represents the complete state needed to remount a volume.
@@ -77,6 +75,9 @@ type MountState struct {
7775
MountParams map[string]string `json:"mountParams"`
7876
MountOptions []string `json:"mountOptions"`
7977
ReadOnly bool `json:"readonly"`
78+
79+
// Daemon process ID (for remount cleanup)
80+
MountDaemonPID int `json:"pid,omitempty"`
8081
}
8182

8283
// Validate checks if the MountState contains required fields.
@@ -105,7 +106,7 @@ type MountStateManager struct {
105106
// It initializes the Kubernetes client and sets up the secret interface.
106107
func NewMountStateManager(namespace string) (*MountStateManager, error) {
107108
if namespace == "" {
108-
namespace = defaultNamespace
109+
namespace = "default"
109110
}
110111

111112
clientset, err := getK8sClient()
@@ -128,35 +129,6 @@ func (sm *MountStateManager) makeSecretName(volumeID string) string {
128129
return secretNamePrefix + hash
129130
}
130131

131-
// GetState retrieves the complete mount state for a specific volume.
132-
// Returns nil without error if no state exists for the volume.
133-
func (sm *MountStateManager) GetState(ctx context.Context, volumeID, targetPath string) (*MountState, error) {
134-
if volumeID == "" {
135-
return nil, fmt.Errorf("volumeID is required")
136-
}
137-
138-
sm.mu.RLock()
139-
defer sm.mu.RUnlock()
140-
141-
secretName := sm.makeSecretName(volumeID)
142-
secret, err := sm.secrets.Get(ctx, secretName, metav1.GetOptions{})
143-
if err != nil {
144-
if errors.IsNotFound(err) {
145-
klog.V(4).Infof("No state found for volume %s", volumeID)
146-
return nil, nil
147-
}
148-
return nil, fmt.Errorf("failed to get state secret %s: %w", secretName, err)
149-
}
150-
151-
state, err := sm.deserializeSecret(secret)
152-
if err != nil {
153-
return nil, fmt.Errorf("failed to deserialize secret %s: %w", secretName, err)
154-
}
155-
156-
klog.V(4).Infof("Retrieved state for volume %s from secret %s", volumeID, secretName)
157-
return state, nil
158-
}
159-
160132
// deserializeSecret converts a Kubernetes Secret into a MountState struct.
161133
func (sm *MountStateManager) deserializeSecret(secret *v1.Secret) (*MountState, error) {
162134
state := &MountState{
@@ -198,6 +170,16 @@ func (sm *MountStateManager) deserializeSecret(secret *v1.Secret) (*MountState,
198170
state.MountOptions = make([]string, 0)
199171
}
200172

173+
// Parse PID if present
174+
if pidStr := byteToString(secret.Data[keyPid]); pidStr != "" {
175+
if pid, err := strconv.Atoi(pidStr); err != nil {
176+
klog.Warningf("Failed to parse PID '%s': %v", pidStr, err)
177+
state.MountDaemonPID = 0
178+
} else {
179+
state.MountDaemonPID = pid
180+
}
181+
}
182+
201183
return state, nil
202184
}
203185

@@ -264,7 +246,6 @@ func (sm *MountStateManager) buildSecret(state *MountState) (*v1.Secret, error)
264246
Labels: map[string]string{
265247
labelAppName: labelValueAppName,
266248
labelComponent: labelValueComp,
267-
labelVolumeID: state.VolumeID,
268249
},
269250
},
270251
Type: v1.SecretTypeOpaque,
@@ -282,6 +263,11 @@ func (sm *MountStateManager) buildSecret(state *MountState) (*v1.Secret, error)
282263
},
283264
}
284265

266+
// Add PID if set
267+
if state.MountDaemonPID > 0 {
268+
secret.StringData[keyPid] = fmt.Sprintf("%d", state.MountDaemonPID)
269+
}
270+
285271
return secret, nil
286272
}
287273

@@ -338,31 +324,6 @@ func (sm *MountStateManager) LoadState(ctx context.Context) ([]*MountState, erro
338324
return states, nil
339325
}
340326

341-
// CleanupStaleStates removes mount state secrets older than the specified duration.
342-
// Useful for cleaning up orphaned secrets from failed mounts.
343-
func (sm *MountStateManager) CleanupStaleStates(ctx context.Context, olderThan time.Duration) (int, error) {
344-
states, err := sm.LoadState(ctx)
345-
if err != nil {
346-
return 0, fmt.Errorf("failed to load states: %w", err)
347-
}
348-
349-
cutoff := time.Now().Add(-olderThan)
350-
deleted := 0
351-
352-
for _, state := range states {
353-
if state.Timestamp.Before(cutoff) {
354-
if err := sm.DeleteState(ctx, state.VolumeID, state.TargetPath); err != nil {
355-
klog.Warningf("Failed to delete stale state for volume %s: %v", state.VolumeID, err)
356-
continue
357-
}
358-
deleted++
359-
klog.V(4).Infof("Deleted stale state for volume %s (age: %v)", state.VolumeID, time.Since(state.Timestamp))
360-
}
361-
}
362-
363-
return deleted, nil
364-
}
365-
366327
func byteToString(value []byte) string {
367328
return string(value)
368329
}

0 commit comments

Comments
 (0)