☰ 👨✈️ Datalayer Local CSI Driver
🚨 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
| Piece | Where |
|---|---|
| Driver package | clouder/csi/ — see the breakdown below |
| Entrypoint | python -m clouder.csi --endpoint unix:///csi/csi.sock --node-id $NODE_ID, only with driver.gatewayOnly: false |
| Image, chart, install | Shared with the gateway: see Node Mounts |
| Check | clouder node-mounts status |
| Tests | clouder/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 semanticsserver.py— gRPC Identity and Nodehealth.pybridge_mount.py— the per-bridge child processproto/— CSI spec v1.11.0 and generated stubsmounter.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 call | What the driver does |
|---|---|
GetPluginInfo / Probe | local.csi.datalayer.io, the Clouder version; always ready |
NodeGetInfo / NodeGetCapabilities | The node name; GET_VOLUME_STATS and VOLUME_CONDITION, no staging |
NodePublishVolume | Validates the volume and its attributes, then starts the bridge filesystem and bind-mounts it at the target path — full rules below the table |
NodeUnpublishVolume | Unmount 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. |
NodeGetVolumeStats | Usage 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 :9808 | Liveness, 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 theCSIDriverhaspodInfoOnMount). - Reads the four attributes and the
mount-token, and refuses:- a missing token;
- a
mount-modethat disagrees with the Pod'sreadOnly; - a
relay-urlthat is notwss://, 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-uidat/csi/mounts/<bridge-uid>/mntif it is not running, waits for it to be mounted, thenmount --bindit at the target path, remountedrowhen the mode isro. - 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
CSIDriverlocal.csi.datalayer.io:attachRequired: false,podInfoOnMount: true(which is what makes kubelet setcsi.storage.k8s.io/ephemeral),volumeLifecycleModes: [Ephemeral],fsGroupPolicy: None; - the upstream
csi-node-driver-registrarsidecar, 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.iomounted at/csi— the socket kubelet calls the driver on./var/lib/kubelet/pods,Bidirectionalpropagation and/dev/fuseare shared with the gateway, which needs all three for its own reasons; The egress torelay.portin theNetworkPolicyoutlived the driver: the gateway'slocal-bridgefilesystem 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:
roandrw- 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-bridgeis 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).