MetalLB IP Allocation Driver — the reference per-class driver for the address-controller core (IP addresses as a first-class resource, cozystack/community#35).
Like a CSI driver for a storage provider, this is the driver for one address backend: it reserves and attaches IPs that are ultimately announced by MetalLB. The core controller owns the generic claim–address lifecycle; this driver owns everything MetalLB-shaped.
The high-level algorithms — provisioning, backend rendering, association, conflict handling — are recorded in docs/design.md, written against the core's contract (address-controller docs/design.md).
An IPAddressClass opts into this driver by naming it:
apiVersion: local.sdn.cozystack.io/v1alpha1
kind: IPAddressClass
metadata:
name: public
spec:
provisioner: metallb.drivers.local.sdn.cozystack.io
reclaimPolicy: Retain
parameters:
addresses: ["203.0.113.0/24"] # CIDRs this class carves fromMetalLB has no native reservation concept, so the driver manufactures
one: a reservation is held by a placeholder Service — selectorless,
type: LoadBalancer, living in a driver-owned namespace — which MetalLB
assigns an address to but, having no endpoints, never announces. Held in
MetalLB's own books, silent on the wire.
Four controllers implement the driver side of the contract:
- Claim — for each family a stamped claim misses: create an
allocation placeholder drawing from the class's pool, let MetalLB (the
allocator of record) assign, observe the address, and record it as a
pre-bound
IPAddress— with the class's reclaim policy, asource.fromClassmarker, and the driver's teardown finalizer. The driver implements Allocate + Pin, not Adopt. - Class — renders the MetalLB configuration per served class: one
IPAddressPool(iad-<class>) withautoAssign: falsecovering the class ranges, and oneL2Advertisementselecting it, owner-referenced to the class. That is the entire per-cluster MetalLB setup — no pool per tenant, no/32s. - Service — the association layer, a separate and reversible act. A
tenant annotates a Service with
local.sdn.cozystack.io/ip-address-claim: <claim>(same namespace, by construction); the driver performs the gap handoff: record the association, release the hold placeholder, pin the workload with MetalLB'smetallb.io/loadBalancerIPs. A second Service referencing an associated claim is rejected loudly. Removing the annotation withdraws the pin (only pins the driver wrote itself, never hand-written ones) and the hold placeholder comes back — the address stays bound: reserved, attached to nothing. - IPAddress — enforces the holding invariant (a placeholder pinned to
the address exists exactly while the address exists and is not
associated to a live workload), clears associations whose Service is
gone, tears down on deletion (delete the placeholder — releasing the
MetalLB allocation — and withdraw any pin), and recovers
Conflictonce no Service wrongfully holds the address.
Packaged as a Helm chart at chart/metallb-iad (no
kustomize). The CRDs belong to the
address-controller chart —
install that first. The driver's ClusterRole
(chart/metallb-iad/templates/role.yaml) is controller-gen output written by
make manifests, never edited by hand.
helm upgrade --install metallb-iad chart/metallb-iad \
--namespace metallb-iad-system --create-namespaceSet metallbNamespace if MetalLB does not live in metallb-system.
Reservation placeholders live in the release namespace by default; set
placeholderNamespace to put them elsewhere (the chart then creates that
namespace). Tenants must have no write access to it — a Service carrying the
placeholder label is exempt from conflict detection only inside that
namespace.
The driver image is published as ghcr.io/cozystack/metallb-iad:main (plus
main-<sha> and semver tags) by the release workflow on every push to main.
make build # vet + build the driver binary
go test ./internal/...
make docker-build docker-push # publish ghcr.io/cozystack/metallb-iad:<git-sha>
make helm-package # lint and package the chart into dist/The chart can install a ValidatingAdmissionPolicy (admissionPolicy.enabled,
off by default) that reserves the Service fields steering MetalLB at a
specific address or pool to the driver and allowlisted principals: the
metallb.io/ and legacy metallb.universe.tf/ loadBalancerIPs,
address-pool and allow-shared-ip annotations, spec.loadBalancerIP,
and the driver's pinned marker. Tenants then get an address only by
naming an IPAddressClaim. Writes that leave these fields unchanged are
always admitted, so controllers that rewrite whole Services keep working.
admissionPolicy.namespaceSelectorscopes the binding. Scope it to tenant namespaces when platform components elsewhere set these annotations themselves; Cozystack's kube-ovn chart, for one, setsallow-shared-ip.admissionPolicy.allowedUsernames/allowedGroupsexempt principals besides the driver's ServiceAccount. The defaults coversystem:mastersand kubeadm'skubeadm:cluster-admins.admissionPolicy.validationActions: [Warn, Audit]reports would-be denials without enforcing them.
- The shared handoff: closing the brief unheld window during
(dis)association with a sequence of
metallb.io/allow-shared-ipService mutations, where the workload's shape permits it. Planned as an opportunistic upgrade over the gap handoff (design doc §5). - Backwards inventory reconciliation (design §6): importing addresses an
eager MetalLB handed to plain Services into
IPAddressobjects after the fact. - MetalLB range syntax beyond CIDRs (e.g.
192.168.9.1-192.168.9.5) in class parameters, and BGP advertisement rendering (L2 only).