Skip to content

Repository files navigation

address-controller

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.

Resource model

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.

Lifecycle

  • Claim: PendingBound (all requested families bound) → Lost (a bound address disappeared). A Dual claim binds one IPv4 and one IPv6 IPAddress and reports both in status.addresses.
  • Address: PendingAvailable (no claimRef) → BoundReleased (claim deleted under Retain; not reusable until an admin clears spec.claimRef, at which point it is Available again). Conflict and Lost are driver-owned phases; the core controller treats them as sticky.

The per-class driver contract

A driver is named by IPAddressClass.spec.provisioner and plugs into the core controller as follows:

  1. Claim pickup. The core controller resolves a claim's class (explicit spec.className, or the class annotated ipaddressclass.local.sdn.cozystack.io/is-default-class: "true") and stamps the claim with the annotation local.sdn.cozystack.io/provisioner: <provisioner>. The driver watches claims carrying its name and provisions for the ones not yet Bound.
  2. Provisioning. The driver allocates from the class's range (spec.source.fromClass: {}) or adopts a provider-side reservation (spec.source.providerRef.id), interpreting IPAddressClass.spec.parameters (opaque to the core). It creates the IPAddress with spec.claimRef pre-set to the claim's namespace/name (UID optional — the core completes it), spec.reclaimPolicy copied from the class, and its own finalizer for backend teardown.
  3. Binding. The core controller completes the binding: the claim goes Bound with the address in status.addresses, the address goes Bound. Statically pre-provisioned Available addresses (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 via spec.addressName.
  4. Reclaim. When the claim is deleted the core controller either marks the address Released (Retain) or deletes the IPAddress object (Delete); in the latter case the driver's finalizer must deallocate the backend resource before allowing the object to go away.
  5. 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 maintains IPAddress.status.associatedTo. The driver also reconciles live Service assignments against the ledger and sets the Conflict phase when an address is held by a Service its binding does not authorize, and Lost when the backing allocation disappears.

The core controller never touches Services, never parses class parameters, and never puts a packet on a wire.

Installing

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-namespace

Helm 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.

Development

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/

About

Core controller for the IPAddressClass feature (local.sdn.cozystack.io) — see cozystack/community#35

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages