Core controller for IPAddressClass/IPAddressClaim/IPAddress (local.sdn.cozystack.io) - #1
Merged
Conversation
kubebuilder init (domain sdn.cozystack.io, group local => API group local.sdn.cozystack.io) plus stub kinds IPAddressClass, IPAddress, IPAddressClaim and controllers for the latter two. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
The PersistentVolume pattern applied to addresses, per the design in cozystack/community#35, under the local.sdn.cozystack.io group: - IPAddressClass (cluster-scoped): provisioner, reclaimPolicy (Retain|Delete, default Retain), opaque driver parameters, and a default-class annotation. - IPAddress (cluster-scoped): the inventory object, with the concrete address, a claimRef, a reclaim policy, and a fromClass/providerRef source union (allocate vs adopt). Phases: Pending, Available, Bound, Released, Conflict, Lost. - IPAddressClaim (namespaced): the tenant-facing request; className (empty means default class), family (IPv4|IPv6|Dual), optional addressName pre-binding; status reports a list of bound addresses so a Dual claim can report both families. Well-known annotations, finalizers, and condition types shared with per-class drivers live in well_known.go. Deepcopy, CRDs, and samples regenerated via make generate manifests. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
The class-agnostic half of the PV-pattern lifecycle: IPAddressClaim reconciler - resolves the claim's class (spec, then sticky status record, then the single default-annotated class; zero or multiple defaults surface as conditions) and stamps the provisioner annotation that per-class drivers key on, - accepts driver pre-bound addresses (completing the claimRef UID), matches Available addresses by class and family, honours addressName pre-binding, and reports Bound/Pending/Lost with conditions, - on deletion runs the reclaim flow behind a protection finalizer: Retain releases the address keeping its claimRef, Delete deletes the IPAddress object so the driver's finalizer can tear down the backend. IPAddress reconciler - phase bookkeeping between Available, Bound, and Released, treating the driver-owned Conflict and Lost phases as sticky, - deletion protection while a live claim holds the address, - safety-net reclaim when a claim vanished without the claim-side flow. A shared field index maps claimRef to addresses. Unit tests use the fake client, replacing the scaffolded envtest suite. RBAC regenerated. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Records the resource model, the claim and address state machines, the exact reconciliation and reclaim algorithms, the field-ownership matrix between core and drivers, and the numbered driver obligations. Also re-stamp the provisioner annotation when a claim is re-targeted at a different class before binding, so the stamp always mirrors the resolved class, as the doc specifies. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Three review questions, answered in the doc and — where the honest answer required it — in the algorithm: - What reclaim actually reclaims: spelled out per policy and per address source in the reclaim-flow section, including that a Retained provider-held reservation deliberately keeps incurring charges until the object is deleted, and that Delete releases backend state through the driver's finalizer (adopted reservations being the driver's documented call). - Why the IP lives in spec, not status: it is identity and intent, not an observation — for a range-carved address the ledger entry is the only record, so there is nothing to re-observe it from; PV precedent; and a backend that cannot pin a specific address cannot be a driver at all. - Class deletion must not break satisfied claims: restructured the claim reconciler so binding state is collected before class resolution and the class is consulted only when families are missing. A fully bound claim now reconciles — including status upkeep — without its class; unresolved-class conditions are recorded in-memory and persisted by the single end-of-pass status update. Regression tests cover a Bound claim with its class deleted and a Pending claim naming a missing class. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
The previous argument overreached twice: it claimed the IP is never re-observable (false for providerRef — describing the provider handle returns it) and implied a provider without choose-at-allocation cannot serve claims (conflating that with the Pin capability, which AWS-style providers have natively via associate-by-handle). The corrected rationale: the split is request vs. record — the claim is the request, and an IPAddress is only ever created after allocation is a fact, so the address is known at creation and no observe-later phase exists; the field provides identity (name derivation, one-object-per-IP via create collision, conflict indexing), which cannot live in a mutable, wipeable status; the fromClass arm has no external record to reconstruct from and the union shares one schema; and the precedent is Service.spec.clusterIP — system-allocated yet spec — rather than PV.spec. providerRef wording widened to cover driver-allocated-at- provider reservations, not just adoption. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
lllamnyp
marked this pull request as ready for review
July 20, 2026 21:00
lllamnyp
added a commit
to lllamnyp/metallb-iad
that referenced
this pull request
Jul 20, 2026
lllamnyp/address-controller#1 landed; the API now comes from main (ac62790) instead of the feat/core-controller branch tip. Both repos are public now, so GOPRIVATE is no longer needed to build. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the core, class-agnostic controller for IP addresses as a first-class resource, per the design proposal in cozystack/community#35 — the analog of the generic PVC/PV binding controller in the storage subsystem. Per-class drivers (the CSI analogue) are a separate project and are out of scope here; this PR defines the contract they plug into.
Resource model
All kinds live in
local.sdn.cozystack.io/v1alpha1(the design doc sketchesipam.cozystack.io; this repo was pointed at thelocal.sdngroup deliberately).IPAddressClass(cluster-scoped):provisioner(immutable, names the driver),reclaimPolicy(Retain|Delete, defaultRetain), opaqueparameters(preserved-unknown-fields object, never parsed by the core). The annotationipaddressclass.local.sdn.cozystack.io/is-default-class: "true"marks the default class.IPAddress(cluster-scoped): the inventory object —address(immutable),className,reclaimPolicy,claimRef(namespace/name/uid), and thesourceunion (fromClass: {}vsproviderRef: {id}, exactly-one enforced by CEL). Phases:Pending,Available,Bound,Released,Conflict,Lost.status.associatedTonames the workload the address is announced for; nil means reserved but inert.IPAddressClaim(namespaced, the whole tenant-facing API):className(empty ⇒ default class),family(IPv4|IPv6|Dual), optionaladdressNamepre-binding.status.addressesis a list so aDualclaim reports both families. Phases:Pending,Bound,Lost.What the core controller does
Claim reconciler — resolves the class (spec → sticky status record → single default-annotated class; zero or multiple defaults surface as conditions and events), stamps the
local.sdn.cozystack.io/provisionerannotation for drivers, accepts driver pre-bound addresses (completing theclaimRefUID), matchesAvailableaddresses by class and family, and reportsBound/Pending/LostwithClassResolvedandBoundconditions. On deletion, a protection finalizer runs the reclaim flow:Retainmarks the addressReleased(keepingclaimRefuntil an admin clears it),Deletedeletes theIPAddressso the driver's finalizer can tear down the backend.Address reconciler — phase bookkeeping (
Available↔Bound↔Released, withReleased+ clearedclaimRef⇒Availableagain), deletion protection while a live claim holds the address, and a safety-net reclaim when the bound claim vanished without the claim-side flow running (including UID-mismatch stale bindings).ConflictandLostare treated as driver-owned sticky states.Per-class driver extension contract
local.sdn.cozystack.io/provisionerannotation and not yetBound.source.fromClass) or adopts (source.providerRef), interpretingclass.spec.parameters, and creates theIPAddresswithclaimRefpre-set (UID optional — the core completes it),reclaimPolicycopied from the class, and its own finalizer for backend teardown.Deletereclaim the core deletes theIPAddress; the driver's finalizer deallocates before the object goes away.local.sdn.cozystack.io/ip-address-claim, writing the backend pin annotation, maintainingstatus.associatedTo, and settingConflict/Lost.Decisions where the design is tentative (it is explicitly a stub on controller mechanics)
volume.kubernetes.io/storage-provisioneranalog) rather than a driver-registration CRD. The design leaves in-tree vs out-of-tree open; the annotation contract works for both and doesn't foreclose a CRD-based capability registry later.Allocate/Adopt/Pinfrom design §4) are not modeled in v1alpha1 — with no capability registry object there is nowhere to declare them yet. A class over a Pin-less backend must be rejected by its driver; revisit when the driver contract gets its own CRD.parametersis an opaque object (preserve-unknown-fields), not StorageClass-stylemap[string]string, so the design'saddresses: ["203.0.113.0/24"]example is valid as written.Pendingwith aMultipleDefaultClassescondition instead of silently picking one (Kubernetes' newest-wins StorageClass behaviour felt too magical for an alpha API).IPAddressobjects bound to one claim — the direction open question 4 leans toward; the status list makes it representable either way.spec.addressNameon the claim (PVCvolumeNameanalog) for pinning a specificAvailableaddress — implied by the "give me this one" discussion and the Released-address reuse flow, not spelled out in the design.status.className, since without an admission webhook the controller can't defaultspec.classNameat create time the way the PV admission plugin does.claimRef(claim deleted and recreated under the same name) is treated as a stale binding and reclaimed, never adopted by the new claim.associatedTo,Conflict) so drivers converge on one shape. TheValidatingAdmissionPolicyfrom the Security section ships with the MetalLB driver, not here.Testing
Hermetic fake-client unit tests for both reconcilers (class resolution and default handling, pre-bound and matched binding, dual-stack, both reclaim policies, deletion protection, sticky phases, stale-UID reclaim). The scaffolded envtest suite was dropped in favour of these; e2e belongs with the drivers.
🤖 Generated with Claude Code