Skip to main content

☰ 👨‍✈️ Datalayer Local CSI Driver

KubernetesRetired (audit 82)A record of the retired path
Retired

🚨 RETIRED — The inline local.csi.datalayer.io CSI volume is retired (audit 82). A folder of a user's own computer is now delivered like every other mount — a Node Mount Gateway process grant on the running Pod (see the Node Mount Gateway page), which the Operator no longer renders as a CSI volume. The chart defaults to gateway-only (driver.gatewayOnly: true), so no CSIDriver object or registrar sidecar is deployed. The rest of this page is a record of the retired path; nothing on a current cluster runs it. plane up forces driver.gatewayOnly=true, and a chart installed by hand with false gets a CSIDriver that no Operator renders a volume for.

How a user's local folder reaches a sandbox now — the same client, the same relay and the same bridge filesystem, started by the gateway into a running Pod — is explained step by step under Node Mounts → A folder of a user's own computer, now.

local.csi.datalayer.io once presented a folder of a user's own computer inside a Datalayer Code Sandbox, through the Contents local bridge — one kind of volume, a bridge to a laptop, and nothing else through it.

It was one of two services in the Datalayer Node Mounts DaemonSet, which now runs the Node Mount Gateway alone. The gateway mounts home folders, datasets, NFS exports, Git checkouts, buckets and the local folder this driver used to deliver, into Pods that are already running; it is not CSI, and it has its own page.

The user-facing behaviour this implements is described under Local Mounts. The contract it consumes is the Contents attachment: a local mount is an attachment with delivery: local-bridge, and nothing below invents a parallel record.

How the pieces fit​

Both ends dial out to the relay. The laptop never opens an inbound port, and the relay never holds the files: it forwards filesystem operations for one bridge, bound to one local root, one sandbox identity, one mount path and one mode.

The contract​

The Operator renders, for an attachment with delivery: local-bridge, an ephemeral inline CSI volume on the runtime Pod:

volumes:
- name: local-mount-1
csi:
driver: local.csi.datalayer.io
readOnly: true # == (mount-mode == "ro")
volumeAttributes:
bridge-uid: <bridge uid>
sandbox-uid: <sandbox uid>
mount-mode: ro # ro | rw
relay-url: wss://<contents host>/api/contents/v1/bridges/<bridge uid> # historical; today wss://<run host>/bridges/<bridge uid>
nodePublishSecretRef:
name: bridge-<bridge uid> # key: mount-token
volumeMounts:
- name: local-mount-1
mountPath: <attachment mount_path>
readOnly: true

Nothing on the Pod names the user's folder. The Secret holds one short-lived mount-token; kubelet hands it to the driver inside NodePublishVolume, so the driver needs no permission to read Secrets. The relay speaks websocket: the first frame is {"role": "mount", "token": <mount-token>}, the rest are binary frames carried by the bridge filesystem.

What ships in Clouder​

PieceWhere
Driver packageclouder/csi/ — see the breakdown below
Entrypointpython -m clouder.csi --endpoint unix:///csi/csi.sock --node-id $NODE_ID, only with driver.gatewayOnly: false
Image, chart, installShared with the gateway: see Node Mounts
Checkclouder node-mounts status
Testsclouder/tests/test_csi_driver.py, test_csi_server.py, test_csi_bridge_mount.py, test_csi_chart.py, test_node_mounts_cli.py

Inside clouder/csi/:

  • driver.py — Node semantics
  • server.py — gRPC Identity and Node
  • health.py
  • bridge_mount.py — the per-bridge child process
  • proto/ — CSI spec v1.11.0 and generated stubs
  • mounter.py (ProcessMounter, FakeMounter) — shared: everything that touches the node's mount table goes through it, the gateway included
  • The gateway's own modules live in the same package — node_mount_gateway*.py, linux.py, git_materializer.py, the process runners — because they share the mounter and the privilege, not because they are CSI

The node driver​

The driver implements the CSI Identity and Node services only; there is nothing to provision, so no Controller service and no StorageClass. It runs one bridge filesystem process per bridge-uid on the node and bind-mounts its mount point into each Pod that names the bridge.

CSI callWhat the driver does
GetPluginInfo / Probelocal.csi.datalayer.io, the Clouder version; always ready
NodeGetInfo / NodeGetCapabilitiesThe node name; GET_VOLUME_STATS and VOLUME_CONDITION, no staging
NodePublishVolumeValidates the volume and its attributes, then starts the bridge filesystem and bind-mounts it at the target path — full rules below the table
NodeUnpublishVolumeUnmount the target, forget the volume, stop the bridge process when the last volume using it is gone. Idempotent: an unknown volume is not an error, though a stale mount at its target is still unmounted.
NodeGetVolumeStatsUsage from statvfs, and volume_condition.abnormal: true with the stable message bridge disconnected: <reason> once the bridge process has exited, target path is not mounted if someone unmounted the target.
GET /healthz, GET /mounts on :9808Liveness, and the bridges and volumes of this node as JSON

NodePublishVolume, in full:

  • Refuses anything but an ephemeral inline volume (csi.storage.k8s.io/ephemeral: "true", set by kubelet because the CSIDriver has podInfoOnMount).
  • Reads the four attributes and the mount-token, and refuses:
    • a missing token;
    • a mount-mode that disagrees with the Pod's readOnly;
    • a relay-url that is not wss://, carries credentials, is on another host than --relay-host, or does not end in /bridges/<bridge-uid>;
    • any attribute that would carry a host path.
  • Starts the bridge filesystem for the bridge-uid at /csi/mounts/<bridge-uid>/mnt if it is not running, waits for it to be mounted, then mount --bind it at the target path, remounted ro when the mode is ro.
  • A second call for the same volume and target is a no-op; a stale mount left at the target by a previous driver incarnation is unmounted first, never bound over.

Revocation, heartbeat expiry, laptop gone. The bridge filesystem process exits when the relay refuses or closes the session. A watcher in the driver notices within --watch-interval seconds (2 by default), unmounts every target the bridge served, stops the process, and keeps the record so that stats report the volume abnormal until the Pod is unpublished. Nothing stale is ever read: a disconnected mount is an unmounted target reported as abnormal, not a folder of old bytes. A kubelet retry of NodePublishVolume after a disconnect starts the bridge again; after a revocation the relay refuses the token, the start fails with UNAVAILABLE, and the Pod stays pending with the reason in its events.

Restricted egress. The driver dials relay-url and nothing else. With --relay-host (chart value relay.host) it refuses a URL on any other host, and the chart's NetworkPolicy allows egress to DNS and to the relay port only — to relay.cidr or relay.namespace/relay.podSelector when set, since a NetworkPolicy cannot select by host name.

Mounters. Everything that touches the mount table goes through a Mounter. ProcessMounter runs on the node: it starts python -m clouder.csi.bridge_mount, which calls code_sandboxes.bridge_mount.run_bridge_mount(relay_url, mount_token, mount_path, mode) with the token in the environment, never on the command line, and exits with that function's return value (4 when the relay refuses the token, 5 when the relay ends a session that was up); it binds with mount --bind and mount -o remount,bind,ro, unmounts with umount, fusermount3 -u -z, then umount -l, and reads /proc/self/mounts so that a dead FUSE mount still counts as mounted and gets cleared. FakeMounter records the same calls in memory for the tests.

What the driver adds to the chart​

The DaemonSet, the ServiceAccount and the NetworkPolicy are described whole under Node Mounts. These existed because of the driver, and are rendered only with driver.gatewayOnly: false:

  • a CSIDriver local.csi.datalayer.io: attachRequired: false, podInfoOnMount: true (which is what makes kubelet set csi.storage.k8s.io/ephemeral), volumeLifecycleModes: [Ephemeral], fsGroupPolicy: None;
  • the upstream csi-node-driver-registrar sidecar, which registers the driver with kubelet. The gateway needs no such thing: nothing registers a Pod annotation;
  • in the DaemonSet, /var/lib/kubelet/plugins/local.csi.datalayer.io mounted at /csi — the socket kubelet calls the driver on. /var/lib/kubelet/pods, Bidirectional propagation and /dev/fuse are shared with the gateway, which needs all three for its own reasons; The egress to relay.port in the NetworkPolicy outlived the driver: the gateway's local-bridge filesystem dials the same relay through it.

Of the values, only registrar.* and driver.gatewayOnly: false were the driver's alone. driver.* configures the one container, relay.* and driver.allowInsecureRelay are what the gateway's bridge filesystem dials, and nodeMountGateway.*, monitoring.* and apiServer.* are the gateway's and the DaemonSet's.

Verify a local mount​

A local mount is verified through the gateway today, with the commands under Node Mounts → Verify. On a current cluster kubectl get csidriver local.csi.datalayer.io answers NotFound, which is expected.

The fixtures the driver had to pass on a cluster:

  • ro and rw
  • kubelet retry after a driver restart
  • Pod deletion
  • laptop disconnect (mount reads as disconnected, not empty)
  • revocation (no read succeeds after DELETE /attachments/{uid})
  • root escape (a path outside the declared root is refused by the local client)

The unit tests cover the driver side of each with the FakeMounter:

cd clouder
python -m pytest clouder/tests/test_csi_driver.py clouder/tests/test_csi_server.py \
clouder/tests/test_csi_bridge_mount.py clouder/tests/test_csi_chart.py \
clouder/tests/test_node_mounts_cli.py -q

The other two pieces​

The bridge relay (Contents)​

Where: src/k8s/services/contents/datalayer_contents/processes/bridge.py and the chart's processes.bridge block. The relay listens on 9402 in the cluster and is reached through the API host's ingress at /bridges/<uid>.

The relay:

  • mints the bridge session when an attachment with delivery: local-bridge is prepared;
  • pairs the local client's stream with the node driver's;
  • forwards operations without interpreting paths beyond refusing anything outside the declared root;
  • closes the session on heartbeat loss or revocation (DELETE /api/contents/v1/bridges/{uid} today) — token first, so a revoked session cannot serve one more read.

The mounting side of that contract is the first websocket frame {"role": "mount", "token": <mount-token>} and a process that exits when the relay refuses.

Rendering the volume (Operator) — removed with the driver​

The Operator used to render, for delivery == "local-bridge", the inline CSI volume shown under The contract, in src/k8s/services/operator/datalayer_operator/services/runtimes.py (_render_local_bridge_volumes / local_bridge_volume). Those functions are gone (audit 82). A local-bridge attachment is now delivered by the Node Mount Gateway as a process grant on the running Pod: the node agent runs the same bridge FUSE filesystem, reading the mount token from a bridge-<bridge uid> Secret the Pod owns, and the attachment travels on the Pod as the contents.datalayer.io/attachments annotation (the bridge's uid only, never its token). An attachment with no bridge session, or one the gateway does not serve, is logged by the Operator (BRIDGE_NOT_PREPARED, GATEWAY_KIND_OFF) and the sandbox launches without it; required is not enforced for a local bridge.

So the driver no longer has an Operator feeding it. Setting driver.gatewayOnly: false deploys the CSIDriver object and the registrar again, but nothing renders a CSI volume that references it — the gateway (with nodeMountGateway.enabled and its _LOCAL_BRIDGES/_CREDENTIALS switches) is the only live path for a local folder. The Operator's whole part is on its page: Contents attachments.

External sandboxes (Daytona, E2B, Modal)​

No CSI driver reaches a sandbox that is not a Pod. There, the same bridge is dialled from inside the sandbox by the same code_sandboxes.bridge_mount filesystem, and only environments that can run FUSE may offer local mounts: an environment that lists the fuse feature (fusepy and /dev/fuse in the image) gets the mount from inside the sandbox when its contents are prepared; Runtimes refuses a local-bridge attachment before launch anywhere else — 422 LOCAL_BRIDGE_UNSUPPORTED — and offers Synchronize instead (see Runtimes).