Persistent storage (CSI)¶
Stateful workloads need persistent volumes. CAPVCO ships the whitesky.cloud CSI driver as an
add-on, so a PersistentVolumeClaim becomes a real whitesky.cloud disk, attached to the node that
needs it. You get standard disks, low-latency physical disks, online expansion, and — with a key
vault — LUKS2 at-rest encryption.
Turn it on¶
Add the CSI label to the Cluster (the base, prebaked and geo templates already do):
CAPVCO then, per cluster:
- deploys the driver (controller Deployments + a node DaemonSet) into the
csi-drivernamespace via a ClusterResourceSet, and - injects a per-cluster credential Secret (
csi-driver-secret) so the driver can create/attach disks with your cluster credential — CAPVCO mints nothing.
Install the CSI add-on once — like the CNI add-on
Like the CNI, the CSI driver is
delivered by a ClusterResourceSet you apply once to the management cluster; every cluster
labelled csi: vco then gets the driver automatically (this is the "per cluster" deploy above).
The driver is the existing whitesky.cloud CSI driver, not a rewrite. Two things about it are
per-instance (white-label), so the bundle carries placeholders you substitute at apply time:
its image (${VCO_CSI_IMAGE}, host = your portal) and its name (${VCO_CSI_DRIVER_NAME}).
The driver names itself from the portal domain — reverse the domain and replace the host leaf with
csi (e.g. try.whitesky.cloud → cloud.whitesky.csi) — and the shipped CSIDriver object and
StorageClass provisioner must use that exact name, so PVCs bind (#21).
From a release (csi.yaml ships alongside cni.yaml):
host=<your-portal-host> # e.g. try.whitesky.cloud
driver=$(echo "$host" | awk -F. '{o="";for(i=NF;i>1;i--)o=o $i".";print o"csi"}') # -> cloud.whitesky.csi
curl -fsSL https://gitlab.whitesky.cloud/api/v4/projects/730/packages/generic/capvco/v0.6.1/csi.yaml \
| VCO_CSI_IMAGE="$host/csi-driver:latest" VCO_CSI_DRIVER_NAME="$driver" \
envsubst '$VCO_CSI_IMAGE $VCO_CSI_DRIVER_NAME' \
| kubectl apply -f -
make csi-addon VCO_CSI_IMAGE=<your-portal-host>/csi-driver:latest | kubectl apply -f -
(it derives the driver name from the image host automatically).
StorageClasses¶
Three classes ship, with stable, instance-independent names (vco-csi*). Their provisioner is
the per-instance driver name, substituted at apply time (above), so they match your running driver on
any instance:
| StorageClass | Backing | Notes |
|---|---|---|
vco-csi |
Standard SDS disk | Default class. WaitForFirstConsumer, allowVolumeExpansion: true. |
vco-csi-physical |
Physical (local, low-latency) disk | diskType: PHYSICAL. For latency-sensitive workloads. |
vco-csi-encrypted-vault |
Standard disk + LUKS2 at-rest | Requires a key vault (below) and the encryption add-on. |
If a PVC sits Pending with no error, check the driver name matched
Provisioning is silent when the StorageClass provisioner doesn't equal the driver's advertised
name — the external-provisioner just ignores the PVC (#21). The apply step above substitutes the
name derived from your portal domain; verify it matches what the driver actually registered:
# against the WORKLOAD cluster — should equal the derived name (e.g. cloud.whitesky.csi)
kubectl get csinode -o jsonpath='{range .items[*].spec.drivers[*]}{.name}{"\n"}{end}'
kubectl get storageclass vco-csi -o jsonpath='{.provisioner}{"\n"}'
VCO_CSI_DRIVER_NAME=<the-name-from-csinode> explicitly when applying the add-on.
A basic PVC just uses the default class:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
# storageClassName omitted → the default class
WaitForFirstConsumer means the disk is created in the right location once a pod actually schedules
— so the volume lands where the workload runs.
ReadWriteOnce is single-node — enforced, but plan for it
Volumes are ReadWriteOnce: the disk attaches to one node at a time. Pods on the same
node share it fine. If a second pod lands on a different node, Kubernetes enforces
single-node attach — the second pod is held with a clean Multi-Attach error for volume … and
waits for the first to release, rather than double-mounting (no corruption path). This is real
(a VolumeAttachment is tracked and the CSIDriver's attachRequired is honoured) and is
validated on a multi-worker cluster by the feature-matrix e2e (#22).
So the failure mode is a pod that waits, not data loss — but design for it: for a disk
per replica use a StatefulSet with volumeClaimTemplates; for a single-owner volume on
a Deployment pin replicas: 1 (or strategy: Recreate) so a rolling update doesn't strand the
new pod behind the old one's attachment.
Provisioned PVs carry no topology nodeAffinity, so when the scheduler places a pod whose
volume already exists it gets no signal about where that volume lives. Placement happens before
any CSI call, so the driver cannot compensate. Within one location that only costs time — the
pod waits for the old node to detach. Across locations it cannot recover: a whitesky.cloud
disk is location-scoped, so on a geo cluster (worker pools pinned to site-1/site-2/site-3,
which may sit in different locations) a pod placed in a different site from its volume waits
indefinitely. Until the driver reports accessible_topology at location granularity (the
remaining driver-side item in #22), keep volume-bound workloads in one location with a
nodeSelector on the driver's own node label:
Online expansion¶
The default and physical classes allow expansion. Grow a bound PVC by editing its request:
The disk and filesystem grow online, no pod restart.
No volume snapshots yet
A VolumeSnapshotClass / CSI snapshotter is not shipped in this release (it needs the
snapshot CRDs + snapshot-controller, which are deferred). Don't rely on CSI snapshots for now;
for cluster-level disaster recovery use etcd backup. Tracked in
known limitations.
Encrypted volumes (LUKS2)¶
The …csi-encrypted-vault class encrypts data at rest with LUKS2, wrapping the volume key with a
Vault/OpenBao transit engine. You choose the vault mode on the VCOCluster:
| Mode | What it does | Status |
|---|---|---|
none (default) |
No vault, no encrypted class — only standard/physical ship. | ✅ |
external |
You bring a Vault/OpenBao; CAPVCO wires the driver to it. | ✅ Supported |
managed |
CAPVCO deploys and operates a Vault for you. | ⏳ Deferred — see known limitations |
For external, point at your vault:
spec:
storage:
encryption:
vault: external
external:
address: https://openbao.example:8200
transitMount: transit
k8sAuthPath: kubernetes
role: csi
caRef:
name: openbao-ca # ConfigMap/Secret with the vault CA bundle
The encryption add-on (csi-encryption: openbao) then delivers the csi-key-backend-config and
openbao-ca ConfigMaps the driver reads. The driver authenticates to the vault with its Kubernetes
ServiceAccount token (via the vault's kubernetes-auth), so no static vault credential lives in the
cluster.
Per-cluster vault auth is a manual step (for now)
Because auth is per-cluster, your vault needs a kubernetes-auth role (bound to the CSI ServiceAccounts, pointing at this cluster's TokenReview endpoint + CA) before encrypted volumes work. Operating the vault and wiring a cluster is covered in OpenBao operations. Automating this per-cluster is a follow-up.
Driver image must include cryptsetup
LUKS formatting needs cryptsetup in the CSI node plugin image. All of CAPVCO's provider-side
wiring (transit login, encrypt/decrypt, the encrypted class) is validated end-to-end, but the
reference …/csi-driver:latest image historically shipped without cryptsetup, in which
case encrypted volumes fail to format. Confirm your driver image includes it. Tracked in
known limitations.
How node ↔ disk identity works¶
The CSI node plugin resolves which whitesky.cloud VM it's running on from the host's SMBIOS UUID
(/sys/class/dmi/id/product_uuid), matched against the VM's reference id — not from the
Kubernetes providerID. So CSI works independently of the provider-id scheme, and there's nothing
for you to configure here.
Security & isolation caveat¶
The injected csi-driver-secret holds your cluster credential. A tenant admin in the workload
cluster can read that Secret and use the credential directly against the whitesky.cloud API — so
the credential's grant scope is the workload cluster's blast radius. Scope the per-cluster
credential to just what one cluster needs.
CSI disk-create needs a location-scoped grant
The driver creates disks through a location-scoped API path. A credential granted only at
the cloudspace level can attach/snapshot but cannot create disks (a genuine 403). So a
cluster running CSI needs a credential with a location grant — which spans every cloudspace
in that location, slightly widening the blast radius beyond one cloudspace. This is the one
isolation trade-off in the current design; spec.storage.credentialScope exists to select it
(location is the default and the working option today). See
known limitations.
Next: give etcd its own fast disk, or grow node disks automatically → Node disks.