Skip to content

Account for IPReservations in IP pool utilization - #13331

Merged
fasaxc merged 5 commits into
projectcalico:masterfrom
fasaxc:ipam-stats-reserved-ips
Jul 31, 2026
Merged

Account for IPReservations in IP pool utilization#13331
fasaxc merged 5 commits into
projectcalico:masterfrom
fasaxc:ipam-stats-reserved-ips

Conversation

@fasaxc

@fasaxc fasaxc commented Jul 27, 2026

Copy link
Copy Markdown
Member

calicoctl ipam show and the kube-controllers IPAM metrics reported reserved addresses as free. GetUtilization counted a block's whole Unallocated list as available and never read the IPReservation resource, so a reservation covering pool space that no block had been carved from was invisible.

GetUtilization now reports Capacity, InUse, Reserved and Available for every pool and every block:

meaning
Capacity addresses in the CIDR
InUse allocated — whether or not also reserved
Reserved reserved — whether or not also allocated
Available neither allocated nor reserved

InUse and Reserved deliberately overlap (an address can be reserved after it was handed out), so Available is computed directly rather than by subtracting the others from Capacity.

Pool-level counts cover the whole pool CIDR, not just the blocks carved from it — a reservation over unblocked space is still unassignable. They are worked out as a set operation (pool minus reservations minus blocks) using go4.org/netipx, so overlapping and nested reservations are counted once: one IPReservation may cover a /24 while another names a single address inside it. Per-block counts keep using the existing addrFilter, so they agree with what allocation will actually do.

calicoctl ipam show gains an IPS RESERVED column and takes all four numbers from the library instead of deriving them. Since the columns can overlap, their percentages no longer necessarily add up to 100.

kube-controllers exports a new ipam_ippool_reserved gauge, labelled ippool like ipam_ippool_size. Existing metrics are unchanged. It does not call GetUtilization for it — the IPAM sync loop shares a goroutine with leak GC, so nothing there should list every block. Instead IPReservation joins the controller's syncer and ipam.NumReservedIPsInCIDR does the pool arithmetic on that cached state; both it and GetUtilization are thin callers of the same set code, so the gauge can't drift from what calicoctl ipam show prints.

Watching the resource needs watch on ipreservations in the kube-controllers ClusterRole: the chart and regenerated manifests are here, and tigera/operator#5115 has the operator side. If the two get out of step the List still succeeds and the controller stays in-sync, so the symptom is a hot re-list rather than a stalled controller.

Also fixes getReservedCIDRs dereferencing a nil CIDR when an IPReservation holds a malformed entry — the surrounding code logs "Ignoring" but did not skip.

New dependency: go4.org/netipx (stdlib-only, ~1.3k LOC).

CORE-13146

Testing

  • New GetUtilization table in libcalico-go/lib/ipam/ipam_test.go covering a reservation inside a block, over pool space with no block, over an already-allocated address, and overlapping/nested reservations.
  • New unit tests for the set arithmetic (reserved_test.go): nesting, duplicates, disjoint, covering the whole pool, IPv6, and saturation for pools larger than an int.
  • New NumReservedAddresses specs in ipam_block_test.go.
  • New unit tests for reservedCIDRs: bare IPs, whitespace, and malformed entries (which covers the nil-CIDR fix directly).
  • New kube-controllers UT driving IPPool and IPReservation KVPairs through the controller's update path and asserting the gauge appears, changes, clears on reservation delete, and is removed on pool delete.
  • Metrics FV creates the IP pool first and the IPReservation second, over pool space no block covers, then waits for the gauge to change and change back on delete — it only passes if the watch is live.
  • Full etcd-backed libcalico-go/lib/ipam suite, the node controller UTs, and the metrics FV against a freshly built image all pass locally.

Release note:

`calicoctl ipam show` and the kube-controllers IPAM metrics no longer report IPs covered by an IPReservation as free. `calicoctl ipam show` has a new IPS RESERVED column and kube-controllers exports a new ipam_ippool_reserved metric.

🤖 Generated with Claude Code

`calicoctl ipam show` and the kube-controllers IPAM metrics reported
reserved addresses as free. `GetUtilization` counted a block's whole
`Unallocated` list as available and never read the `IPReservation`
resource, so a reservation covering pool space that no block had been
carved from was invisible.

`GetUtilization` now reports `Capacity`, `InUse`, `Reserved` and
`Available` for every pool and every block. `InUse` and `Reserved`
overlap when an address was allocated before it was reserved, so
`Available` is computed directly instead of by subtraction. Pool-level
counts cover the whole pool CIDR, worked out as a set operation (pool
minus reservations minus blocks) so that overlapping or nested
reservations count once and space with no block yet is included.

`calicoctl ipam show` gains an IPS RESERVED column and takes all four
numbers from the library. kube-controllers exports a new
`ipam_ippool_reserved` gauge; the existing metrics are unchanged.

Also stops `getReservedCIDRs` dereferencing a nil CIDR when an
`IPReservation` holds a malformed entry, which the surrounding code
already meant to skip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 12:41
@fasaxc
fasaxc requested review from a team as code owners July 27, 2026 12:41
@marvin-tigera marvin-tigera added this to the Calico v3.33.0 milestone Jul 27, 2026
@marvin-tigera marvin-tigera added release-note-required Change has user-facing impact (no matter how small) docs-pr-required Change is not yet documented labels Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes IPAM utilization reporting so that addresses covered by IPReservation are no longer counted as free in both calicoctl ipam show and kube-controllers IPAM metrics. It extends GetUtilization to surface Capacity, InUse, Reserved, and Available per pool and per block, including reservations that apply to pool CIDR space that has not yet been carved into blocks, using go4.org/netipx for correct set arithmetic with overlapping/nested reservations.

Changes:

  • Extend libcalico-go IPAM utilization reporting to account for IPReservation across pools/blocks and expose new utilization fields.
  • Update calicoctl ipam show to display the new reserved column and consume utilization numbers directly from the library.
  • Add ipam_ippool_reserved Prometheus gauge in kube-controllers and add unit/FV test coverage; update IPAM design docs accordingly.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
typha/deps.txt Add go4.org/netipx to Typha dependency manifest.
node/deps.txt Add go4.org/netipx to Node dependency manifest.
libcalico-go/lib/ipam/reserved.go New helper for pool-wide set arithmetic with reservations and blocks.
libcalico-go/lib/ipam/reserved_test.go Unit tests for pool/reservation/block set arithmetic, including overlap and IPv6.
libcalico-go/lib/ipam/ipam.go Teach GetUtilization to compute Capacity/InUse/Reserved/Available and read IPReservations; fix malformed reservation handling.
libcalico-go/lib/ipam/ipam_types.go Extend utilization structs with new fields and clarify overlap semantics.
libcalico-go/lib/ipam/ipam_test.go Add e2e datastore tests for utilization with reservations and overlap cases.
libcalico-go/lib/ipam/ipam_block.go Add per-block NumReservedAddresses helper.
libcalico-go/lib/ipam/ipam_block_test.go Unit tests for NumReservedAddresses.
libcalico-go/deps.txt Add go4.org/netipx to libcalico-go dependency manifest.
kube-controllers/pkg/controllers/node/metrics_fv_test.go FV expectations for new reserved metric; add helper to create IPReservation.
kube-controllers/pkg/controllers/node/ipam.go Add and publish ipam_ippool_reserved gauge and clear it on pool deletion.
kube-controllers/pkg/controllers/node/ipam_test.go UT coverage that reserved gauge is published only for tracked pools and cleared on deletion.
kube-controllers/pkg/controllers/node/fake_client.go Implement GetUtilization in fake IPAM client for metrics unit tests.
kube-controllers/deps.txt Add go4.org/netipx to kube-controllers dependency manifest.
hack/deps.txt Add go4.org/netipx to hack tooling dependency manifest.
go.sum Add go4.org/netipx checksums.
go.mod Add go4.org/netipx module requirement.
felix/deps.txt Add go4.org/netipx to Felix dependency manifest.
e2e/deps.txt Add go4.org/netipx to e2e dependency manifest.
design/ipam/ipam-other-callers.md Document calicoctl ipam show columns now being sourced from GetUtilization.
design/ipam/ipam-gc.md Document kube-controllers reserved metric behavior and caveats.
design/ipam/ipam-datastore.md Note IPReservation is read by GetUtilization for reporting correctness.
design/ipam/ipam-core-library.md Document new GetUtilization semantics and overlap rules.
confd/deps.txt Add go4.org/netipx to confd dependency manifest.
cni-plugin/deps.txt Add go4.org/netipx to CNI plugin dependency manifest.
cmd/deps.txt Add go4.org/netipx to cmd dependency manifest.
calicoctl/deps.txt Add go4.org/netipx to calicoctl dependency manifest.
calicoctl/calicoctl/commands/ipam/show.go Add “IPS RESERVED” column and consume library-provided utilization fields.
app-policy/deps.txt Add go4.org/netipx to app-policy dependency manifest.
apiserver/deps.txt Add go4.org/netipx to apiserver dependency manifest.

Comment thread go.mod
Comment thread kube-controllers/pkg/controllers/node/ipam.go Outdated
fasaxc and others added 2 commits July 27, 2026 14:27
Ask GetUtilization only for the pools the controller reports on.  Left
empty it also totals the pseudo-pool that orphaned blocks are listed
under, which has no gauge.  The fake now honours `args.Pools`, so the
existing test proves the request is scoped rather than the results being
filtered afterwards.

Fold the `go4.org/netipx` require into the direct block instead of
leaving it standalone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two CI failures from the kube-controllers metrics FV.

The new gauge's help text named `ipam_allocations_in_use`, and the FV
asserts metric absence with a substring match, so the help text made an
absence check match after the allocations had gone.  Dropped the
cross-reference and left a note by the metric.

The reservation test assumed the gauge would refresh within 10s of
creating an IPReservation, but the controller has `list` and not `watch`
on the resource, so a reservation change does not wake the sync loop; the
gauge refreshes on the next sync from another cause or on the periodic
one.  The test now creates the reservation and then the pool, so pool
creation is what triggers the sync.  Documented the cadence on the metric
and in design/ipam/ipam-gc.md, including what it would take to make it
prompt (a watch plus a ClusterRole change in the chart and in
tigera/operator).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread libcalico-go/lib/ipam/reserved.go Outdated
Comment on lines +39 to +41
// /24 while another names a single IP inside it, and an L2 subnet contributes
// its network and broadcast addresses on top — so the counting is a set
// operation rather than a sum over the CIDRs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

L2 is enterprise only, either leave out the reference or flag that it's ent-only

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Claude: Dropped the L2 clause — overlapping IPReservations justify the set arithmetic on their own, so the comment reads fine without it. The enterprise side documents the subnet-edge source where it is added, so nothing is lost there. Removed the same clause on the enterprise branch too, so the file stays identical across the two and future picks stay clean.

Comment thread libcalico-go/lib/ipam/reserved.go Outdated
L2 subnets are an enterprise-only source of reserved addresses, so the
comment in `countPoolSpace` should not cite them; overlapping
IPReservations already justify the set arithmetic on their own.

Pool validation keeps pools far below the point where a count would not
fit in an int, so say the saturation is defensive rather than implying it
is reachable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@caseydavenport caseydavenport left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lookin' pretty good - just one comment about putting GetUtilization on the critical path. WDYT?

Comment thread libcalico-go/lib/ipam/ipam.go
updateReservedMetrics called GetUtilization on every IPAM sync, which lists
every allocation block.  The node controller's sync loop shares a goroutine
with leak GC and has been overloaded before, and the controller threw all of
that block work away: it read only PoolUtilization.Reserved.

kube-controllers now watches IPReservation on its own syncer and counts the
covered addresses per pool with a new ipam.NumReservedIPsInCIDR, so the sync
loop makes no datastore request for the metric.  The arithmetic is still the
library's - GetUtilization and the new helper are both thin callers of the set
code in reserved.go - so the gauge still agrees with `calicoctl ipam show`.
The helper takes IPReservations rather than CIDRs so a variant can take a
second kind of reserving resource without reshaping its callers.

Watching the resource needs `watch` in the kube-controllers ClusterRole; the
chart and manifests carry it here, and tigera/operator needs the matching
change.  The gauge's help text loses its staleness caveat: a reservation
change now wakes the loop, which the metrics FV asserts by creating the
reservation after the pool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fasaxc fasaxc added the needs-operator-pr PRs that require follow-on operator work label Jul 28, 2026
@fasaxc
fasaxc merged commit fc6a9af into projectcalico:master Jul 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cherry-pick-candidate docs-pr-required Change is not yet documented needs-operator-pr PRs that require follow-on operator work release-note-required Change has user-facing impact (no matter how small)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants