Tenant Storage (CSI)¶
Tenant clusters get persistent storage via kubevirt-csi-driver running in a split topology: the CSI controller lives in the under cluster, the CSI node lives in the tenant cluster, and the bridge between the two is the under cluster's KubeVirt API plus a per-tenant kubeconfig.
This page documents how that split is wired up and how per-tenant quota is enforced.
Topology¶
Under Cluster Tenant Cluster
───────────────── ─────────────────
[ kubevirt-csi-controller ] ───▶ (PV claims arrive here)
Deployment in
tenant namespace [ kubevirt-csi-node ]
DaemonSet
[ tenant-storage-class [ csi.kubevirt.io ]
ResourceQuota ] CSIDriver
[ kmetal-webhook [ kubevirt
ValidatingWebhook ] StorageClass ]
A few things to notice:
- The controller side is one Deployment per tenant cluster (not per tenant), named
kubevirt-csi-controller-<cluster>, in the tenant's under-cluster namespace alongside the cluster'sTenantControlPlanepod. A tenant namespace that hosts multiple tenant clusters runs one controller Deployment per cluster. - Each controller Deployment runs 2 replicas with leader election and the standard CSI sidecars (
csi-provisioner,csi-attacher,csi-snapshotter,csi-resizer, plus the driver itself). - The node side is injected into the tenant cluster as a DaemonSet plus a
csi.kubevirt.ioCSIDriver object and akubevirtStorageClass. - Tenant workloads see one StorageClass (
kubevirt) andPersistentVolumeClaims through it. The tenant doesn't see — or talk to — KubeVirt or the under cluster's underlying StorageClass directly. - Behind the scenes, every tenant PVC becomes a KubeVirt
DataVolumeon the under cluster's backing storage. The DataVolume is attached as an extra disk to the tenant's worker VMs, which is what the node-side CSI driver presents to the kubelet.
Per-tenant quota¶
The platform enforces a single tenant-wide storage cap through two cooperating pieces:
ResourceQuotain the tenant's under-cluster namespace on the resourcetenant-storage-class.storageclass.storage.k8s.io/requests.storage. This is the hard ceiling — the sum of all DataVolume sizes provisioned for that tenant cannot exceed it. The quota is enforced by the under cluster's API server when thekubevirt-csi-controllercreates a DataVolume.kmetal-webhookDeployment +ValidatingWebhookConfigurationpushed into the tenant cluster. A small Deployment in the tenant's under-cluster namespace (started with--storage-class=tenant-storage-class) pushes aValidatingWebhookConfigurationinto the tenant cluster whose URL points back at the under-cluster webhook Service. When a tenant creates a PVC inside their cluster, the webhook reads the matchingResourceQuotaand rejects the request up front if the cap would be exceeded.
The two pieces enforce the same number — the second one just surfaces the rejection at PVC admission time inside the tenant cluster (with a clear ResourceQuota … exhausted message), instead of letting the request flow through and fail later at DataVolume creation in the under cluster.
A tenant that hits its quota gets a normal Insufficient storage quota error on PVC creation. There's no cross-tenant amplification — storage requests from one tenant cannot affect another tenant's allocation.
What this means for the tenant¶
Inside the tenant cluster, the integration is invisible:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: kubevirt
resources:
requests:
storage: 5Gi
That claim binds, the tenant's pod mounts it, and the data lives on whatever production storage the under cluster uses (Ceph, an enterprise array, a vendor CSI driver — the under-cluster admin's choice).