Kubernetes operator for VirtFoundry private cloud (virtfoundry.io CRDs).
Canonical desired state lives in Custom Resources. The REST API and UI in virtfoundry/core are optional clients of that API (adoption / GitOps-friendly layer).
| Kind | Reconciler | Notes |
|---|---|---|
| Tenant | Namespace + isolation + status | Creates virtfoundry-tenant-{slug} with PSA, default-deny NetworkPolicy, ResourceQuota, LimitRange |
| Instance | KubeVirt VM + status sync | Writes VM/VMI status; Multus/VPC NICs by default (pod network opt-in only) |
Other kinds (VPC, Network, Disk, Instance create/delete) are defined as CRDs; controllers are planned per core design spec.
spec.slug must be unique across Tenants. The controller indexes .spec.slug
and marks colliding Tenants Failed (terminal) so they never share or wipe a
namespace. Admission-time uniqueness is deferred to validating webhooks
(follow-up #26).
The reconciler only writes to virtfoundry-tenant-{slug} namespaces that carry
virtfoundry.io/tenant={slug} and either no controller ownerRef (adopted once)
or an ownerRef pointing at that Tenant. Deletes require label and a matching
controller ownerRef. Anything else — system namespaces, unlabelled namespaces,
another Tenant's namespace, label-only legacy namespaces — is refused, and the
Tenant reports status.phase: Failed (or drops its finalizer on delete) instead
of adopting or wiping it.
The chart adds a matching cluster-side guard: namespaceGuard.enabled (default
true) installs a ValidatingAdmissionPolicy that denies the operator
ServiceAccount any Namespace DELETE outside that set. It renders only on
clusters serving admissionregistration.k8s.io/v1 policies (Kubernetes >= 1.30).
kubevirtGuard.enabled (default true) installs a ValidatingAdmissionPolicy
that denies the operator ServiceAccount CREATE/UPDATE/DELETE on
kubevirt.io VirtualMachines and VirtualMachineInstances outside
virtfoundry-tenant-*. The manager ClusterRole is monitor-only on KubeVirt;
mutate is granted per tenant via RoleBinding to *-kubevirt-mutate (issue
#28). The VAP remains
defense-in-depth if RoleBindings are minted outside tenant namespaces.
crAdmission.enabled (default true) installs a ValidatingAdmissionPolicy that:
- Rejects
InstanceCREATE/UPDATE outsidevirtfoundry-tenant-* - Rejects
Offeringwith CPU outside1..256ormemoryMioutside64..1048576 - Rejects
TemplatewithsourceType: containerwhosespec.imageis empty, HTTP(S), or outside the ContainerDisk allowlist (imageAllowlist.prefixes, defaultquay.io/containerdisks/+quay.io/kubevirt/)
Offering bounds are also in the CRD OpenAPI schema. The Instance reconciler
refuses out-of-namespace Instances, out-of-bounds Offerings, and unlisted
Template images with status.phase: Failed (defense in depth when VAP is
unavailable). Quantity parsing never uses resource.MustParse on guest
CPU/memory. ISO Templates are not subject to the container allowlist at
admission (CDI import path).
Still open in #26:
- Validating webhooks + cert-manager + Helm
:9443(including admission-time slug uniqueness) - Broader privileged KubeVirt feature rejection at admission
The manager no longer starts an empty webhook TLS server.
On every successful Tenant reconcile the operator also ensures:
| Object | Name | Purpose |
|---|---|---|
| PSA labels | pod-security.kubernetes.io/{enforce,audit,warn}=privileged |
Required for KubeVirt virt-launcher |
| NetworkPolicy | virtfoundry-default-deny |
Default-deny ingress/egress; allow DNS to kube-system, same-namespace, kubevirt/cdi namespaces, and TCP 80/443 for containerDisk/CDI pulls |
| ResourceQuota | virtfoundry-default |
Caps pods, CPU/memory requests+limits, PVCs, Services |
| LimitRange | virtfoundry-default |
Sensible container default request/limit/max |
Instance reconcile only runs in namespaces labelled virtfoundry.io/tenant under
the virtfoundry-tenant-* prefix. Guest VMs do not get the KubeVirt pod
network (masquerade) by default — attach Multus/VPC networks via spec.nics, or
opt in with annotation virtfoundry.io/allow-pod-network=true (breaking change
vs ≤0.7). ContainerDisk images must match the allowlist (quay.io/containerdisks/,
quay.io/kubevirt/ by default; chart imageAllowlist.prefixes /
VIRTFOUNDRY_ALLOWED_CONTAINER_IMAGE_PREFIXES). dedicatedCPU /
KubeVirt DedicatedCPUPlacement is applied only when the referenced Offering
is labelled virtfoundry.io/platform-owned=true (issue #23); otherwise
Offering.spec.dedicatedCPU and Instance.spec.dedicatedCPU are ignored.
On existing VMs the reconciler converges:
| Instance field | KubeVirt target |
|---|---|
powerState |
spec.runStrategy |
nics (+ Network status NAD) |
template.spec.networks + domain.devices.interfaces |
templateRef / Template image |
managed containerdisk volume image |
cloudInitSecretRef / legacy cloudInitUserData + Template + sshKeyRefs |
managed cloudinitdisk userdata (Secret preferred) |
Managed volumes (containerdisk, cloudinitdisk) are replaced by name; foreign
volumes (e.g. future PVC disks) are preserved and managed orphans are dropped.
Still create-time only (not converged on update): Offering CPU/memory
requests, dedicatedCPU domain placement, and full VMI template label rewrite.
make generate manifests
make test
make buildkind create cluster --name virtfoundry-op
make install
make deploy IMG=virtfoundry-operator:dev
# or for local iterate:
make run
kubectl apply -f config/samples/virtfoundry_v1alpha1_tenant.yaml
kubectl get vf-tenantApache-2.0 — see LICENSE and NOTICE.
GOVERNANCE.md · MAINTAINERS.md · CONTRIBUTING.md · SECURITY.md · CODE_OF_CONDUCT.md