Skip to content

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:

  1. A Provider CR — declares the existence of a backend in this cluster and carries its connection settings (endpoints, credential references, defaults). One per backend instance.
  2. 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.

flowchart LR user[User] -->|placement.providerSelector| vm[(VirtualMachine)] vm --> mc[banlieue-controller] mc -->|creates| infra[(VSphereMachine)] infra --> pv[banlieue-provider-vsphere] pv -->|talks to| backend[vSphere API]

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.name references a ProviderClass CR, which carries the backend kind (spec.backend, drawn from a well-known set: cloud-hypervisor, vsphere, libvirt and proxmox) 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's Providers.
  • connection.credentialsRef points at a Secret. Credentials are not embedded in the CR.
  • capabilities is the explicit capability declaration: every storage / network class a VMClass or VMImage may 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 in status.failureDomains[].
  • A cluster can have many Providers, including multiple of the same class (prod-vsphere, dr-vsphere, lab-vsphere). One backend host should be one Provider: capacity is counted per Provider, 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 in status.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).

    spec:
      scheduling:
        cpuOvercommitRatio: 2   # at most 2 vCPUs per published CPU
    

    examples/01-provider-cloud-hypervisor.yaml is a complete Cloud Hypervisor Provider with this field and the capacity it is applied to. - When provisioning Kubernetes clusters via CAPI, a VSphereCluster (InfraCluster) aggregates the status.failureDomains[] of one or more Providers — 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 failureDomain to 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:

  1. Watch its infrastructure CRD (VSphereMachine, VSphereMachineTemplate, etc.).
  2. 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-party banlieue-libvirt RPC/XDR client for libvirt (ADR-0011), and the first-party banlieue-proxmox REST client for Proxmox VE (ADR-0074). No third-party Go/Python client libraries are vendored — everything is native Rust.
  3. Report status uniformly. Patch .status on the infra CR with the CAPI v1beta2 condition vocabulary, regardless of how the backend natively surfaces errors. See Infrastructure CRDs & CAPI.
  4. Add and clear finalisers so deletes block until the backend is actually torn down.
  5. Use server-side apply with a provider-specific field manager (banlieue.io/provider-vsphere, etc.) — so ownership of fields is explicit.
  6. 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 whose spec.providerRef.name is 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.
  7. Prove it is alive by renewing its Lease. The SDK's leader election renews a coordination.k8s.io Lease named banlieue-provider-<ProviderClass>-<Provider> in the workload namespace every 5 seconds. banlieue-operator reads it into status.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 own Ready=True (ADR-0102).
  8. 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 extraConfig key banlieue.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).
  9. 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.capacity with what guests may use there: cpus, memoryMiB (after anything the backend reserves, or a smaller budget it enforces), and memoryOverheadMiBPerVM (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 Provider credentials 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 around kube::runtime::Action.

Writing a third-party provider

banlieue's provider model is open. To ship a new provider:

  1. 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.
  2. 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 with banlieue_provider_sdk::naming::workload_name, and grant your identity that Lease alone, never Leases cluster-wide.
  3. 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.