Skip to content

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's TenantControlPlane pod. 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.io CSIDriver object and a kubevirt StorageClass.
  • Tenant workloads see one StorageClass (kubevirt) and PersistentVolumeClaims 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 DataVolume on 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:

  1. ResourceQuota in the tenant's under-cluster namespace on the resource tenant-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 the kubevirt-csi-controller creates a DataVolume.
  2. kmetal-webhook Deployment + ValidatingWebhookConfiguration pushed into the tenant cluster. A small Deployment in the tenant's under-cluster namespace (started with --storage-class=tenant-storage-class) pushes a ValidatingWebhookConfiguration into 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 matching ResourceQuota and 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).