Skip to content

Name an existing MetalLB pool instead of rendering one per class - #8

Merged
Timofei Larkin (lllamnyp) merged 3 commits into
mainfrom
feat/class-references-pool
Nov 13, 2026
Merged

Timofei Larkin (lllamnyp) merged 3 commits into
mainfrom
feat/class-references-pool

Conversation

@lllamnyp

@lllamnyp Timofei Larkin (lllamnyp) commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Implements decision 0003 of the ip-address-claims proposal (cozystack/community#124): a class names an existing MetalLB IPAddressPool instead of listing CIDRs for the driver to render. Stacked on #5, which moves the module to github.com/cozystack/address-controller and provides the auto-claim annotation constants.

Changes

  • Class parameters. parameters.pool names a pool in the MetalLB namespace. parameters.addresses is rejected by name, and unknown fields are rejected.

  • No rendering. The class controller no longer creates iad-<class> pools or L2Advertisements. The admin owns the pool and its advertisement. The ClusterRole drops write access to both kinds.

  • Refusal. A class is refused for new reservations if:

    • its pool is missing;
    • the pool auto-assigns (MetalLB treats an absent autoAssign as true);
    • another class of this driver names the same pool.

    The class gets a PoolRefused event, and each claim that would be provisioned gets one too. A refusal blocks only new allocation placeholders: a placeholder MetalLB already assigned is still recorded, and existing addresses stay held. Claims and classes are re-reconciled on pool changes.

  • --lb-class / loadBalancerClass value. This is the --lb-class of the MetalLB instance the driver fronts. Every placeholder carries it as spec.loadBalancerClass. A class marked is-auto-claim-class gets an AutoClaimUnservable event when the value is non-empty, because auto-claimed Services carry none.

  • Holds. A hold placeholder names the class's pool. If the class's parameters name none, it holds by the pin alone, which MetalLB honours from any pool that admits the Service.

  • Docs. Design §3 is rewritten, the "L2 only, CIDRs only" gap is removed, and the README gains the new class shape, a migration section, and steps for reserving part of a pool that auto-assigns today. The design doc records why MetalLB objects stay unstructured: its API types live in MetalLB's root module, whose requirements would set this module's Kubernetes and controller-runtime versions.

Migration

After upgrading, a class with parameters.addresses gets no new reservations, and its existing addresses stay held. To migrate, remove the class's owner references from the rendered iad-<class> pool and advertisement, then set parameters.pool: iad-<class>. The README has the commands. The owner references must go first: otherwise deleting the class later deletes the pool.

Not in this PR

  • IPAddressClass.status.loadBalancerClass. The core API has no such field yet.
  • The §5 serviceability check, including the pool's serviceAllocation. MetalLB applies it to placeholders and to pinned consumer Services alike, so both sides go into that check together. Until then the docs state what a pool's serviceAllocation must admit.

go test ./internal/..., golangci-lint run and helm lint pass.

A class now names an IPAddressPool in parameters.pool, which the admin
owns with its advertisement; the driver no longer renders a pool and
L2Advertisement from parameters.addresses. The class controller and the
claim controller refuse a class whose pool is missing, auto-assigns,
does not admit placeholders through serviceAllocation, or is named by
another class; a refusal blocks only new allocation placeholders.
Placeholders draw from the named pool and carry the --lb-class of the
MetalLB instance the driver fronts, and an auto-claim class served by a
non-default instance gets a warning. This follows decision 0003 of the
ip-address-claims proposal.

Assisted-By: LLM
Signed-off-by: Timofei Larkin <[email protected]>
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7828a188-6386-4cab-bd6d-3b10bdac33e2
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: name an existing MetalLB pool instead of rendering one (implements community#124)

The core of #124 is here and reads well. parameters.pool replaces parameters.addresses, the driver no longer renders the pool or the advertisement, and ParseClassParameters rejects the old addresses key with a migration message and requires a pool (internal/driver/driver.go). The four refusals are in classPool (internal/controller/pools.go:44-73): invalid parameters, pool missing, PoolProblem, and another class naming the same pool. classesNamingPool is provisioner-scoped, excludes the class itself, and sorts (pools.go:77-93). Treating an unset autoAssign as auto-assigning is correct, since MetalLB defaults it to true. --lb-class is wired to the placeholder's spec.loadBalancerClass and to the AutoClaimUnservable warning. Good.

Two problems, one of them a silent data-plane failure.

[MAJOR] A pool scoped by serviceAllocation passes the class check but the consumer never gets the address

PoolProblem checks the pool's serviceAllocation against the placeholder only: admitsNamespace(alloc, placeholderNS) and serviceSelectors against {PlaceholderLabel: MarkerValue} (internal/driver/driver.go, PoolProblem). The consumer Service must satisfy the same serviceAllocation for MetalLB to assign it the pinned address. I confirmed against MetalLB source that this is enforced on the explicit paths, not only on auto-assign: Assign (the pinned-IP path) and AllocateFromPool (the named-pool path, which calls Assign) both run isPoolCompatibleWithService, which checks ServiceAllocations.Namespaces and ServiceAllocations.ServiceSelectors. A Service that does not match gets AllocationFailed and no address.

Nothing makes the consumer match. associate writes only metallb.io/loadBalancerIPs and the pinned annotation on the consumer (internal/controller/service_controller.go), not a label, and serviceSelectors match labels, not annotations. So:

  • A pool whose serviceAllocation.namespaces lists the placeholder namespace but not the tenant's: the class is accepted (placeholder matches), the reservation holds, and the pin on the tenant Service is rejected by MetalLB. The address is reserved and announced nowhere, with the class reported healthy.
  • A pool with any serviceSelectors: the consumer carries no matching label and no code path adds one, so it can never match.

This is exactly the defense-in-depth §8 recommends ("IPAddressPool.spec.serviceAllocation can restrict the pool to a controller-set label or namespace"). Following that advice silently breaks announcement. The consumer-side serviceability check of §5 is unimplemented, so nothing catches it. The "controller-set label" the design mentions has no implementation: the driver would need to stamp a label on associated Services for a serviceSelectors-scoped pool to work, and to validate the consumer's namespace, not the placeholder's, against serviceAllocation.namespaces. At minimum the check should validate the consumer, and a serviceSelectors-scoped pool should be refused outright until the label is stamped, rather than accepted and left silent.

[MEDIUM] status.loadBalancerClass is not reported, so #124's loadBalancerClass mechanism is unwired on this side

#124 has the provisioner "report it as status.loadBalancerClass on every class it serves," and §5 has a controller "read the class's status.loadBalancerClass first." The ClassReconciler writes no status at all (internal/controller/class_controller.go, Reconcile emits events only), and the imported API ([email protected], which predates #124) has no Status on IPAddressClass. So the value lives only on the driver's flag and on placeholder spec.loadBalancerClass; it is never surfaced where §5 reads it. This looks like staging behind a core API bump, which is reasonable, but it should be stated in the PR, and until it lands the §5 serviceability and loadBalancerClass checks cannot run on either side.

[LOW] The serviceAllocation matcher is a parallel reimplementation

admitsNamespace and matchesAnySelector (internal/driver/driver.go) re-derive MetalLB's namespace and selector matching. They look right for the current fields (empty admits all, namespaces OR namespaceSelectors, any serviceSelector matches), but a second implementation drifts from the backend over time, MetalLB's priority, or a future serviceAllocation field, would not be reflected. Worth a test against a real MetalLB config, or a note that the semantics are pinned to a MetalLB version.

The pool-reference model itself is a clear improvement and is implemented cleanly. The serviceAllocation consumer gap is the one to resolve before merge.

Assisted-By: LLM
Signed-off-by: Timofei Larkin <[email protected]>
The class check vetted a pool's serviceAllocation for placeholders only,
with a copy of MetalLB's matching, while consumer Services, which MetalLB
checks against the same serviceAllocation, went unchecked. Both sides
belong to the design's serviceability check, which this change does not
implement. The class check now refuses only what decision 0003 lists:
invalid parameters, a missing or auto-assigning pool, and a pool named
by two classes. The docs state what serviceAllocation must admit.

Assisted-By: LLM
Signed-off-by: Timofei Larkin <[email protected]>
@lllamnyp

Copy link
Copy Markdown
Member Author

Thanks for the review. Changed in d9b11fc; per point:

[MAJOR] serviceAllocation consumer gap. The mechanism is right: MetalLB checks serviceAllocation in Assign and AllocateFromPool, so a pinned consumer the pool does not admit gets AllocationFailed. The consumer check is out of scope for this PR, though.

  • It is the §5 serviceability check. The proposal lists that check as specified and not implemented, and this PR's description lists it under Not in this PR.
  • The "controller-set label" in §8 has never had an implementation. Adding it means a new label on tenant Services, and deciding whether the admission policy gates it. That belongs with the serviceability check, not with replacing CIDRs by a pool reference.
  • The failure is not silent: MetalLB records AllocationFailed on the Service. The real defect is that the association is recorded before the pin is tried, which is exactly what the serviceability check exists to prevent.

What did need fixing here was the placeholder-only serviceAllocation check. It went beyond the refusals decision 0003 lists, and it made the class look vetted when only half was. d9b11fc removes it. The class check now refuses exactly what 0003 lists: invalid parameters, a missing pool, an auto-assigning pool, and a pool named by two classes. Design §3 and the README state that a pool's serviceAllocation must admit the placeholder namespace and every namespace that names claims of the class, and that the driver does not check this yet. Both sides of serviceAllocation will go into the serviceability check together.

[MEDIUM] status.loadBalancerClass. This is already stated, under Not in this PR: "IPAddressClass.status.loadBalancerClass. The core API has no such field yet." One correction: it does not block the §5 checks on the driver side. The driver knows its value from --lb-class. The status field is for third parties that create Services and must set loadBalancerClass at creation.

[LOW] Reimplemented matcher. Removed with the check in d9b11fc. When serviceability brings it back, it will carry the MetalLB version it follows. Importing MetalLB's types instead was considered and rejected; design §3 gives the reason: they live in MetalLB's root module, whose requirements would set this module's Kubernetes and controller-runtime versions.

IvanHunters
IvanHunters previously approved these changes Nov 6, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of d9b11fc

The MAJOR is resolved the right way. The placeholder-only serviceAllocation check is gone, PoolProblem now refuses only an auto-assigning pool, so the class check matches decision 0003 exactly: invalid parameters, a missing pool, an auto-assigning pool, and a pool named by two classes. The consumer-side serviceAllocation requirement is now documented in the README and the design doc, attributed to the §5 serviceability check and marked not-yet-implemented, and the docs record that MetalLB enforces serviceAllocation on pinned Services too. That is the right scope: the false "class vetted" signal is gone and the remaining check sits where it belongs.

On status.loadBalancerClass: agreed, and my earlier framing was off. The driver has its value from --lb-class, so the driver-side §5 check does not need the status field; it is third parties creating Services that do, which is listed under Not in this PR with the core API field pending.

Checked the rest against the merged design and the code: the driver no longer creates or deletes a pool or advertisement, so an admin pool survives class deletion; claim_controller resolves the pool per claim and does not tear down an existing placeholder when the pool develops a problem; NewPlaceholder leaves spec.loadBalancerClass nil for the default implementation and sets it only for a non-empty --lb-class, both covered by tests; and the old parameters.addresses is refused with a migration hint. Tests pass.

LGTM.

@lllamnyp
Timofei Larkin (lllamnyp) deleted the branch feat/placeholder-auto-claim-opt-out November 6, 2026 19:01
@lllamnyp
Timofei Larkin (lllamnyp) changed the base branch from feat/placeholder-auto-claim-opt-out to main November 6, 2026 19:02
@lllamnyp
Timofei Larkin (lllamnyp) dismissed IvanHunters’s stale review November 6, 2026 19:02

The base branch was changed.

@lllamnyp

Copy link
Copy Markdown
Member Author

Its base PR merged, so this now targets main. Retargeting dismissed the approval; the content is unchanged since it was approved and it merges cleanly. Needs a re-approval.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving. The branch is unchanged since my review of d9b11fc (same head), so that review stands: the pool-reference model is clean, the class check matches decision 0003, and the serviceAllocation consumer check is correctly deferred to the §5 serviceability work. LGTM.

@lllamnyp
Timofei Larkin (lllamnyp) merged commit 1faf937 into main Nov 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants