Skip to content

Architecture

How CAPVCO is put together, for operators and contributors who want the model beneath the guides. If you just want to use a feature, the guides are the place; this page explains why things are shaped the way they are.

The big picture

flowchart LR
  subgraph mgmt["Management cluster"]
    capi["CAPI core + kubeadm providers"]
    capvco["CAPVCO manager"]
    creds[("per-cluster<br/>credential Secret")]
  end
  subgraph vco["whitesky.cloud"]
    cs["Cloudspace (per cluster)"]
    cp["Control-plane VMs"]
    wk["Worker VMs"]
    lb["Control-plane LB / endpoint"]
  end
  capi -->|Cluster / KCP / MachineDeployment| capvco
  capvco -->|reads| creds
  capvco -->|"REST /api/1 + /alpha"| vco
  cs --- cp
  cs --- wk
  lb --> cp

The management cluster holds Cluster API + CAPVCO. CAPVCO watches the CAPI objects (Cluster, KubeadmControlPlane, MachineDeployment and its own VCOCluster / VCOMachine) and drives the whitesky.cloud REST API to realise them. It holds no standing credential — it resolves a per-cluster credential from the VCOCluster's credentialSecretRef for every backend call.

Two objects, two controllers

CRD Controller ensures
VCOCluster The cloudspace(s), the VPN mesh (geo), the registry (air-gapped), the control-plane endpoint/LB, and then injects add-on credentials + opt-in backup / image-publishing / drift detection.
VCOMachine One VM: creates/deletes it, attaches disks (data + etcd), registers it in the control-plane LB pool (control-plane machines), grows disks, and reports addresses + conditions.

VCOClusterTemplate and VCOMachineTemplate are the template shapes CAPI's Cluster / KubeadmControlPlane / MachineDeployment stamp instances from.

Cloudspace per cluster

Each cluster gets its own whitesky.cloud cloudspace — network + tenancy isolation. The control-plane endpoint is published on it: by default an ingress L4 load balancer (HA-capable, ≥ 3 control-plane nodes), or a port-forward (single control plane). For geo clusters there is one cloudspace per location, meshed by site-to-site VPN, with a health-aware DNS control-plane endpoint instead of a single LB.

The endpoint (LB + public IP) is allocated before any node exists, so control-plane certificates can be minted against a stable controlPlaneEndpoint from the very first boot.

Machines and provider-id

A VCOMachine maps 1:1 to a VM. The bootstrap userdata (from the kubeadm bootstrap provider) is submitted as whitesky.cloud userdata with the CAPI provider-id vco://<cloudspaceId>/<machineUID> merged in, so the node registers with exactly the provider-id the controller expects.

Node ↔ VM identity for CSI is separate: the CSI driver resolves its VM from the SMBIOS product_uuid, independent of the provider-id. So storage works regardless of the provider-id scheme.

Credential model

CAPVCO follows the CAPI cluster-identity pattern: the operator supplies a distinct, narrowly- scoped credential per cluster (VCOCluster.spec.credentialSecretRef), and the controller builds a per-cluster API client from it (cached by the Secret's resourceVersion). The manager mints nothing and holds no standing credential.

A few operations (VM-image publishing, the registry TLS cert) are operator-privileged and take a separate operator credential. The full model — keys, rotation, operator-privileged ops, and the credentials injected into workload clusters — is in Configuration.

Add-ons via ClusterResourceSet

Cluster-scoped features are delivered as static manifests through ClusterResourceSet (CRS), selected by labels on the Cluster, with per-cluster Secrets injected directly by the controller (a CRS can't template Secret data):

Label Add-on
…/cni CNI (Calico; veth_mtu pinned to 1350 so any cluster is geo/VPN-safe)
…/csi whitesky.cloud CSI driver + StorageClasses (+ injected csi-driver-secret)
…/csi-encryption Key-backend config for encrypted volumes
…/ccm Cloud-controller-manager (node init + Service-LB)
…/service-lb Standalone Service-type=LoadBalancer controller
…/external-dns ExternalDNS + the whitesky.cloud DNS webhook
…/apiserver-proxy Internal-API-path proxy (geo)

The full label→value→component mapping is in Configuration.

Reconciliation order

The VCOCluster controller ensures things in an order that respects dependencies:

flowchart LR
  cs["cloudspace(s)"] --> mesh["mesh (geo)"] --> reg["registry (air-gapped)"] --> ep["control-plane endpoint"] --> add["add-on creds + backup / images / drift"]

VCOMachine reconciliation, in turn, creates the VM (stopping first if an etcd disk must be attached pre-boot), registers control-plane machines in the LB pool by API-server health, and evaluates disk growth. Everything is idempotent (adopt-by-name / adopt-by-address) and tolerant of 404 on delete, so retries and partial failures converge rather than duplicate or wedge.

Geo provisioning is all-or-nothing — on a hard failure the controller tears down everything it created, so you never get a half-built geo cluster.

Status and conditions

Both CRDs report real backend state back as conditions, so clusterctl describe cluster tells the truth. Notable VCOCluster conditions: CloudspaceReady, MeshReady (geo), EndpointReady, BackupReady, KeyEscrowed (amber until you acknowledge a provider-generated backup key), DriftDetected (informational). VCOMachine reports readiness, vmId, addresses, and disk state.

Where to go next

  • The exact CRD fields: CRD API reference (generated from the Go types).
  • The honest edges: Known limitations.
  • Design rationale in depth: cluster-api-plan.md in the repository — a per-milestone design and validation narrative.