Provider Model¶
A provider is the unit of pluggability in banlieue. A provider is whatever
turns a uniform VirtualMachine request into a real VM on a specific backend
(Cloud Hypervisor, vSphere, libvirt, Proxmox VE, …).
This page explains what a provider is on the wire, what it implements, and how it plugs in. For the reasoning behind the design, see Abstraction principle and CRD-only contract.
Two things make a provider¶
A provider is two artifacts:
- A
ProviderCR — declares the existence of a backend in this cluster and carries its connection settings (endpoints, credential references, defaults). One per backend instance. - A provider controller — a Kubernetes controller that watches an
infrastructure CRD (e.g.
VSphereMachine) and drives the corresponding backend.
The user only ever sees (1). The controller (2) is plumbing.
The Provider CR¶
apiVersion: banlieue.io/v1alpha1
kind: Provider
metadata:
name: prod-vsphere
spec:
providerClassRef:
name: vsphere
connection:
endpoint: https://vcenter.example.com/sdk
credentialsRef:
name: vsphere-creds
insecureSkipTLSVerify: false
capabilities:
storageClasses:
- name: gold
target: { datastore: ssd-tier-0 }
networkClasses:
- name: prod
target: { portGroup: vmnet-prod }
Notes:
providerClassRef.namereferences aProviderClassCR, which carries the backend kind (spec.backend, drawn from a well-known set:cloud-hypervisor,vsphere,libvirtandproxmox) plus install metadata (image, resources, RBAC) without changing this reference. See ADR-0012.- The user never sees the
connection:/capabilities:blocks. They're owned by whoever administers the cluster'sProviders. connection.credentialsRefpoints at aSecret. Credentials are not embedded in the CR.capabilitiesis the explicit capability declaration: every storage / network class aVMClassorVMImagemay request must be listed here for this provider to be considered by the scheduler. The provider's controller then verifies them and reports per-failure-domain availability instatus.failureDomains[].- A cluster can have many
Providers, including multiple of the same class (prod-vsphere,dr-vsphere,lab-vsphere). One backend host should be oneProvider: capacity is counted perProvider, so two pointing at one host would each count only their own VMs. -
scheduling.cpuOvercommitRatio(1 to 64, default 4) is how many vCPUs the scheduler may place per CPU a failure domain publishes instatus.failureDomains[].attributes.capacity. Memory is never overcommitted. Capacity is published today by the Cloud Hypervisor provider only; a failure domain without it is not checked (ADR-0107).examples/01-provider-cloud-hypervisor.yamlis a complete Cloud HypervisorProviderwith this field and the capacity it is applied to. - When provisioning Kubernetes clusters via CAPI, aVSphereCluster(InfraCluster) aggregates thestatus.failureDomains[]of one or moreProviders — so a single cluster can span multiple vCenters. See Infrastructure CRDs & CAPI.
Naming note: "failure domain" and "availability zone" mean the same thing here. The field is called
failureDomainto align with the CAPI v1beta2 vocabulary (clusterv1.FailureDomain,Machine.spec.failureDomain) that banlieue's infra CRDs are built to satisfy — read "availability zone" wherever that's more natural.
The authoritative type is in
crates/banlieue-api/src/banlieue/provider.rs.
For a full deploy walkthrough see the
vSphere provider guide.
Cordoning a provider¶
To stop new VMs landing on a provider, for maintenance, label it:
kubectl -n banlieue-system label provider hv01 banlieue.io/unschedulable=true
kubectl -n banlieue-system label provider hv01 banlieue.io/unschedulable- # uncordon
The scheduler then skips it for every new placement, right after providerSelector
(ADR-0124). VMs already on it stay where
they are and the provider keeps looking after them: a VM's own provider stays a candidate for
it, so the cordon is never drift and never recreates it. Only the value "true" cordons. A VM
that only cordoned providers could take (every candidate cordoned, or one pinned to a cordoned
host by its failureDomainSelector) waits with Scheduled=False, reason
ProviderUnschedulable, and a message naming those providers.
What a provider controller does¶
A provider controller is a Kubernetes controller. Its responsibilities:
- Watch its infrastructure CRD (
VSphereMachine,VSphereMachineTemplate, etc.). - Reconcile to the backend. Translate the uniform spec into native API
calls — the first-party
vim_rs-based BYOC HTTP client for vSphere (ADR-0008), the first-partybanlieue-libvirtRPC/XDR client for libvirt (ADR-0011), and the first-partybanlieue-proxmoxREST client for Proxmox VE (ADR-0074). No third-party Go/Python client libraries are vendored — everything is native Rust. - Report status uniformly. Patch
.statuson the infra CR with the CAPI v1beta2 condition vocabulary, regardless of how the backend natively surfaces errors. See Infrastructure CRDs & CAPI. - Add and clear finalisers so deletes block until the backend is actually torn down.
- Use server-side apply with a provider-specific field manager
(
banlieue.io/provider-vsphere, etc.) — so ownership of fields is explicit. - Serve only its own Provider's machines. One provider process runs per
Provider, but every machine of its kind is visible to it; it reconciles only those whosespec.providerRef.nameis its--provider-name(banlieue_provider_sdk::reconciler::serves). Acting on another Provider's machine fails on credentials at best, and at worst finalizes it against the wrong backend. - Prove it is alive by renewing its Lease. The SDK's leader election
renews a
coordination.k8s.ioLease namedbanlieue-provider-<ProviderClass>-<Provider>in the workload namespace every 5 seconds. banlieue-operator reads it intostatus.workload.heartbeat, and the scheduler places nothing new on a provider whose Lease has stopped (Stale), because a provider that is down cannot clear its ownReady=True(ADR-0102). - Mark what it creates, and touch nothing unmarked. Each backend object
carries the machine's UID, written by the call that creates it (the
Proxmox VM description, the vSphere
extraConfigkeybanlieue.machine.uid). A crash between creating an object and recording it in status is then recoverable, and a same-named object somebody else owns is never adopted or destroyed (ADR-0075, ADR-0101). - Publish capacity, if it can measure it. Optional, but it is what lets
the scheduler stop placing VMs on a full failure domain. Fill
status.failureDomains[].attributes.capacitywith what guests may use there:cpus,memoryMiB(after anything the backend reserves, or a smaller budget it enforces), andmemoryOverheadMiBPerVM(what each VM costs beyond its class's memory). Publish only figures you measured; leave out any you cannot, and the scheduler does not check that resource. The Cloud Hypervisor provider is the reference (provider.rs::guest_capacity) (ADR-0107).
What a provider controller does not do¶
- It does not speak directly to the banlieue main controller. The bus is the K8s API. See CRD-only contract.
- It does not publish a service. There is nothing to expose, nothing to authenticate against, nothing to load-balance.
- It does not mutate
VirtualMachine. It only mutates its own infra CR. Status mirroring is the main controller's job. - It does not see
Providercredentials except by resolving the Secret it was pointed at.
Anatomy of a provider crate¶
Each provider is a library crate — it has no main.rs. The single
banlieue binary owns the one entrypoint and dispatches the
banlieue provider <name> subcommand into the provider's run()
(ADR-0004).
All providers share banlieue-provider-sdk:
crates/banlieue-provider-vsphere/
├── Cargo.toml # [lib] only — no [[bin]]
└── src/
├── lib.rs # library root; `pub use app::{Cli, run}`
├── app.rs # the `banlieue provider vsphere` subcommand: Cli (clap::Args) + run(); wires up the Provider, VMImage, and VSphereMachine controllers
├── context.rs # reconciler Context
├── error.rs # typed errors
├── client/ # backend-agnostic vSphere client surface
│ ├── mod.rs # VSphereClient / VSphereClientFactory traits
│ ├── vim.rs # production impl (vim_rs)
│ └── fake.rs # in-memory fake for unit tests
└── reconciler/
├── mod.rs
├── provider.rs # Provider inventory walk → failureDomains[]
├── vmimage.rs # per-zone Kairos template build/import (ADR-0020/0021)
└── vspheremachine.rs # VSphereMachine create path: CloneVM_Task + PowerOnVM_Task (ADR-0024); create-path only, no update/drift/live-migration yet
The crate exports Cli (a clap::Args payload) and pub async fn run(cli);
the banlieue binary embeds Cli as the provider vsphere subcommand and
calls run(). The reconcilers depend only on the VSphereClient trait, so
they're unit-tested against an in-memory FakeClient without compiling or
connecting through vim_rs.
The SDK
(banlieue-provider-sdk)
gives you:
client::build_client— Kubernetes client with sensible timeouts.status::set_condition/find_condition/is_condition_true— CAPI-shaped condition handling.finalizer::ensure_finalizer/remove_finalizer— patch-based finalisers.ssa::server_side_apply— server-side apply with a per-provider field manager.reconciler::{requeue_default, requeue_on_error, requeue_long, no_requeue}— small helpers aroundkube::runtime::Action.
Writing a third-party provider¶
banlieue's provider model is open. To ship a new provider:
- Define your infrastructure CRD(s) in a separate crate, satisfying the
CAPI v1beta2 InfraMachine contract.
The provider CRD shape used in
crates/banlieue-api/src/infrastructure/(VSphereMachine,VSphereMachineTemplate) is the reference. - Build a controller against your CRD using
banlieue-provider-sdk. Its leader election gives you the Lease that liveness reads (item 7 above); name it withbanlieue_provider_sdk::naming::workload_name, and grant your identity that Lease alone, never Leases cluster-wide. - Ship a container image and a Kustomize manifest.
That's it. No registration with the banlieue project. No code changes to
banlieue-api. No coordination with the main controller team. The whole point
of the CRD-only contract is that you
don't have to be us to add a provider.