The core, class-agnostic controller for IP addresses as a first-class resource (design: cozystack/community#35). It is the analog of the generic PVC/PV binding controller in the storage subsystem: it owns claim–address binding, status, and finalizer-driven cleanup, and leaves actual address allocation to per-class drivers (the CSI analogue), which live in separate projects.
The full contract — state machines, reconciliation algorithms, field ownership, and driver obligations — is specified in docs/design.md. The reference driver implementing it is metallb-iad.
All kinds live in the local.sdn.cozystack.io/v1alpha1 API group.
| storage | addresses | scope | role |
|---|---|---|---|
StorageClass |
IPAddressClass |
cluster | which pool, which driver (spec.provisioner), which reclaim policy |
PersistentVolume |
IPAddress |
cluster | the address itself, with a claimRef, a reclaim policy, and a fromClass/providerRef source union |
PersistentVolumeClaim |
IPAddressClaim |
namespaced | "give me one" — the whole tenant-facing API |
A tenant creates an IPAddressClaim, reads status.addresses[].address, and
puts it in DNS. The claim survives the workload: deleting the Service that
used the address never releases the address, because the address's lifetime
belongs to the claim, and (under reclaimPolicy: Retain) even outlives the
claim as a Released IPAddress.
- Claim:
Pending→Bound(all requested families bound) →Lost(a bound address disappeared). ADualclaim binds one IPv4 and one IPv6IPAddressand reports both instatus.addresses. - Address:
Pending→Available(noclaimRef) →Bound→Released(claim deleted underRetain; not reusable until an admin clearsspec.claimRef, at which point it isAvailableagain).ConflictandLostare driver-owned phases; the core controller treats them as sticky.
A driver is named by IPAddressClass.spec.provisioner and plugs into the
core controller as follows:
- Claim pickup. The core controller resolves a claim's class (explicit
spec.className, or the class annotatedipaddressclass.local.sdn.cozystack.io/is-default-class: "true") and stamps the claim with the annotationlocal.sdn.cozystack.io/provisioner: <provisioner>. The driver watches claims carrying its name and provisions for the ones not yetBound. - Provisioning. The driver allocates from the class's range
(
spec.source.fromClass: {}) or adopts a provider-side reservation (spec.source.providerRef.id), interpretingIPAddressClass.spec.parameters(opaque to the core). It creates theIPAddresswithspec.claimRefpre-set to the claim's namespace/name (UID optional — the core completes it),spec.reclaimPolicycopied from the class, and its own finalizer for backend teardown. - Binding. The core controller completes the binding: the claim goes
Boundwith the address instatus.addresses, the address goesBound. Statically pre-provisionedAvailableaddresses (created by an admin or a driver ahead of demand) are matched to claims by class and family; a claim may pin a specific one viaspec.addressName. - Reclaim. When the claim is deleted the core controller either marks
the address
Released(Retain) or deletes theIPAddressobject (Delete); in the latter case the driver's finalizer must deallocate the backend resource before allowing the object to go away. - Association. Attaching a bound address to a workload is a separate,
reversible act and is entirely driver territory: the driver resolves the
Service annotation
local.sdn.cozystack.io/ip-address-claim(naming a claim in the Service's own namespace), writes the backend's pin annotation, and maintainsIPAddress.status.associatedTo. The driver also reconciles live Service assignments against the ledger and sets theConflictphase when an address is held by a Service its binding does not authorize, andLostwhen the backing allocation disappears.
The core controller never touches Services, never parses class parameters, and never puts a packet on a wire.
Packaged as a Helm chart at chart/address-controller
(no kustomize). The CRDs and the ClusterRole are generated by make manifests
straight into the chart — chart/address-controller/crds/ and
templates/role.yaml are controller-gen output, never edited by hand.
helm upgrade --install address-controller chart/address-controller \
--namespace address-controller-system --create-namespaceHelm installs the crds/ directory on first install but never upgrades it;
after an API change, make install-crds applies the current CRDs to the
cluster. helm uninstall leaves the CRDs (and every IPAddress ledger
entry) behind by design.
The controller image is published as
ghcr.io/lllamnyp/address-controller:main (plus main-<sha> and semver tags)
by the release workflow on every push to main.
make manifests generate # regenerate CRDs, ClusterRole, and deepcopy after API changes
make build # build the manager
go test ./internal/controller/
make run # run the controller locally
make docker-build docker-push # publish ghcr.io/lllamnyp/address-controller:<git-sha>
make helm-package # lint and package the chart into dist/