refactor(cni): extract galactic-ipam as a real delegated CNI IPAM plugin - #304
refactor(cni): extract galactic-ipam as a real delegated CNI IPAM plugin#304privateip wants to merge 3 commits into
Conversation
d5959ba to
2b74a81
Compare
2b74a81 to
f226227
Compare
…ation
This step rewired galactic-cni/galactic-tap-cni's own IPAM handling
from an in-process ipam.Allocate() call into real CNI IPAM delegation
(ipam.ExecAdd, execing whatever binary "ipam.type" names). The e2e
test's config still had the old "ipam": {"type": "pool"} block from
before this step -- "pool" is no longer a mode selector, it's now
looked up as a literal binary name, so ADD failed with "failed to find
plugin \"pool\" in path [/opt/cni/bin]" (confirmed on PR #303/#304's
own CI run, the first time e2e has actually executed against a live
cluster in this whole plugin-chain-split stack, now that a separate
pre-existing eBPF-artifact-drift issue blocking Build no longer blocks
it).
Fixed to "ipam": {"type": "galactic-ipam", "ipv6_subnet": "..."} --
the explicit contract this step establishes (see internal/cniipam's
doc comment). Also fixed CNI_PATH from /opt/cni/bin to / so
ipam.ExecAdd's lookup can actually find /galactic-ipam: every binary
in the chain lands at the image root
(containers/galactic-cni/Dockerfile), not /opt/cni/bin -- that path
only exists on a real host once installer.Bootstrap's init container
stages it there, which this test's pod never runs. Dropped
GALACTIC_CNI_ENABLE_LOCAL_IPAM=true from the script: it's now inert
for this config (an explicit ipv6_subnet doesn't need the local-IPAM
fallback), though harmless either way since this step doesn't remove
the env var itself.
BGP/SRv6/eBPF publish is still inline in galactic-tap-cni's own cmdAdd
at this point in the split (that only moves out to its own chained
binary in a later step), so the eBPF control daemon startup, bpf-fs
mount, and hostNetwork pod spec stay exactly as they were -- this
config still needs the same BGPRouter fixture and pre-pinned eBPF maps
the previous step's fix already established.
Verification: task lint (0 issues), go build/vet clean, task test:unit
full suite green. Not verified against a live cluster locally;
pushing to let CI's own test-e2e job confirm, same as the previous
step's fix.
"fd00:e2e:tap::/48" isn't a valid IPv6 literal -- "tap" contains 'p', not a hex digit. cniipam's own parseConf validates ipv6_subnet with net.ParseCIDR (see internal/cniipam/config.go's validateIPv6Subnet), so ADD failed with "invalid CIDR value for field 'ipam.ipv6_subnet'" as soon as this step's own IPAM-delegation fix (previous commit) made that field's value actually reach real validation for the first time -- it was never exercised before since PR #303/#304's e2e job never got this far until the eBPF-artifact-drift and wrong-binary-target issues in earlier commits on this branch were fixed. Fixed to "fd00:e2e::/48": drops the invalid "tap" hextet, keeps "e2e" (itself valid hex) as the mnemonic. Verification: task lint (0 issues), go build/vet clean, task test:unit full suite green, plus a plain net.ParseCIDR-equivalent check (Python's ipaddress.ip_network) confirming the literal parses.
|
Went through this one too. The delegation wiring itself is fine and the build/unit tests are clean, but moving IPAM state from in-memory/CRD-backed to durable on-disk marker files changes some failure-mode assumptions that the rest of the ADD/DEL/CHECK lifecycle wasn't updated to match. A few of these look like they should block merge, roughly in order of how bad I think they are. Rollback on a failed ADD never releases the IPAM allocation. The checked-in NAD configs weren't migrated to the new shape. All ten There's no migration path from the old CRD-annotation-based allocation record to the new on-disk markers. A pod allocated before this ships has no marker file. After the upgrade, a new pod's ADD scans on-disk state, doesn't see that old subnet as used, and can allocate it to someone else, so you end up with two live pods sharing an address.
CHECK never validates IPAM state.
There's also a locking bug: And a performance concern on top of that: the site-wide IPv4 pool can hold on the order of 4000 entries, and Two smaller things: The allocation leak on failed ADD, the un-migrated NAD configs, and the missing upgrade path for existing allocations feel like the ones worth resolving before this merges, since they're silent correctness and availability problems rather than edge cases. |
Sequencing step 1 of the CNI plugin-chain split. galactic-ipam is now a
genuine CNI IPAM delegation target (github.com/containernetworking/plugins/
pkg/ipam.ExecAdd/ExecDel), not the in-process library call internal/cniipam
was left as at the end of step 0 — galactic-cni and galactic-tap-cni now
exec it, passing their own StdinData straight through (it already carries
the "ipam" block; the delegate ignores everything else in that JSON).
Lands the explicit IPAM contract the design note called for, ahead of any
real external IPAM system needing it:
- Whether IPAM runs at all is decided solely by whether "ipam" is present
in a master plugin's own config — no environment variable can trigger or
suppress that anymore. The old GALACTIC_CNI_ENABLE_LOCAL_IPAM "enforce
ipam block" check in internal/cni/cnitap's parseConf is gone outright.
- ipv6_subnet/ipv4_subnet/address_families move inside the "ipam" block
(internal/cniipam.IPAM's own fields) instead of living as PluginConf
siblings. Their CIDR validation moved with them, into galactic-ipam's own
parseConf — the master plugins no longer validate fields they don't own.
- ipam.type now names the delegated binary ("galactic-ipam"), not a
pool-vs-static mode selector. Mode is decided internally, from field
presence: static_ip selects the static path, otherwise ipv6_subnet/
ipv4_subnet select pool mode (either family alone, or both).
- enableLocalIPAM is renamed GALACTIC_IPAM_ENABLE_LOCAL_IPAM
(internal/config/ipam.go) and moves entirely into galactic-ipam's own
process: it's read once, by the delegate, and only fills in a default
IPv6 pool CIDR when "ipam" is present but specifies neither static_ip
nor a subnet for either family. It can no longer manufacture an "ipam"
block out of thin air.
The other half of this step — and the reason delegation could become
self-contained at all — is IPv6 pool persistence. PoolAllocator
(internal/cni/ipam) tracked allocations in memory only, which is silently
safe for ADD (each pool belongs to exactly one VPCAttachment, so there's
never a collision to guard against) but useless for DEL: a fresh process
has nothing to look up. Deallocation used to close that gap by reading the
allocated value back out of the BGPAdvertisement CRD annotation galactic-bgp
wrote — the one thing that would have kept galactic-ipam coupled to
galactic-bgp's CRD-naming scheme and a Kubernetes client it otherwise has
no use for at all. Fixed by giving PoolAllocator the same on-disk marker
file IPv4PoolAllocator already had (keyed by containerID, guarded by an
flock, one lock directory shared by both families now:
ipam.DefaultLockDir). Both allocators gained a DeallocateContainer(
containerID) and a peek-only LookupContainer(containerID), so galactic-ipam
now looks its own allocations up locally, and doesn't import a k8s client,
crdnames, or bgpv1alpha1 at all — decision 2 from the review notes, fully
realized: no kubeconfig, no RBAC surface, nothing.
internal/cniipam's shape changed accordingly: no more AllocConfig wrapper
(nothing needs vpc/vpcattachment/namespace once dealloc is local) — Allocate
and Deallocate/DeallocateContainer now take *IPAM directly. It gained its
own cmdAdd/cmdDel/cmdCheck/cmdStatus (github.com/containernetworking/cni/
pkg/skel) and CHECK logic that genuinely didn't exist before (confirming a
containerID's marker file is still present), plus a small result-conversion
helper (ResultToIPAMResult/BuildCNIResult) both sides of the delegation
boundary use to stay in the ips/routes-only shape the IPAM protocol expects
— no interfaces, ever; that stays the master plugin's job.
cmd/galactic-ipam, Taskfile.yaml, the galactic-cni Dockerfile, and
installer.Bootstrap all pick up the new binary the same way step 0 wired in
galactic-tap-cni.
Verified: task lint, task build, and task test:unit are all green
(internal/cniipam at 91% coverage, including a real ADD -> CHECK -> DEL ->
CHECK round trip proving the self-contained-by-containerID design holds
end to end). task test:e2e not run, same caveat as step 0's PR.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ation
This step rewired galactic-cni/galactic-tap-cni's own IPAM handling
from an in-process ipam.Allocate() call into real CNI IPAM delegation
(ipam.ExecAdd, execing whatever binary "ipam.type" names). The e2e
test's config still had the old "ipam": {"type": "pool"} block from
before this step -- "pool" is no longer a mode selector, it's now
looked up as a literal binary name, so ADD failed with "failed to find
plugin \"pool\" in path [/opt/cni/bin]" (confirmed on PR #303/#304's
own CI run, the first time e2e has actually executed against a live
cluster in this whole plugin-chain-split stack, now that a separate
pre-existing eBPF-artifact-drift issue blocking Build no longer blocks
it).
Fixed to "ipam": {"type": "galactic-ipam", "ipv6_subnet": "..."} --
the explicit contract this step establishes (see internal/cniipam's
doc comment). Also fixed CNI_PATH from /opt/cni/bin to / so
ipam.ExecAdd's lookup can actually find /galactic-ipam: every binary
in the chain lands at the image root
(containers/galactic-cni/Dockerfile), not /opt/cni/bin -- that path
only exists on a real host once installer.Bootstrap's init container
stages it there, which this test's pod never runs. Dropped
GALACTIC_CNI_ENABLE_LOCAL_IPAM=true from the script: it's now inert
for this config (an explicit ipv6_subnet doesn't need the local-IPAM
fallback), though harmless either way since this step doesn't remove
the env var itself.
BGP/SRv6/eBPF publish is still inline in galactic-tap-cni's own cmdAdd
at this point in the split (that only moves out to its own chained
binary in a later step), so the eBPF control daemon startup, bpf-fs
mount, and hostNetwork pod spec stay exactly as they were -- this
config still needs the same BGPRouter fixture and pre-pinned eBPF maps
the previous step's fix already established.
Verification: task lint (0 issues), go build/vet clean, task test:unit
full suite green. Not verified against a live cluster locally;
pushing to let CI's own test-e2e job confirm, same as the previous
step's fix.
"fd00:e2e:tap::/48" isn't a valid IPv6 literal -- "tap" contains 'p', not a hex digit. cniipam's own parseConf validates ipv6_subnet with net.ParseCIDR (see internal/cniipam/config.go's validateIPv6Subnet), so ADD failed with "invalid CIDR value for field 'ipam.ipv6_subnet'" as soon as this step's own IPAM-delegation fix (previous commit) made that field's value actually reach real validation for the first time -- it was never exercised before since PR #303/#304's e2e job never got this far until the eBPF-artifact-drift and wrong-binary-target issues in earlier commits on this branch were fixed. Fixed to "fd00:e2e::/48": drops the invalid "tap" hextet, keeps "e2e" (itself valid hex) as the mnemonic. Verification: task lint (0 issues), go build/vet clean, task test:unit full suite green, plus a plain net.ParseCIDR-equivalent check (Python's ipaddress.ip_network) confirming the literal parses.
1d28639 to
578d55b
Compare
Stack (merge bottom to top):
Summary
Second branch in the CNI plugin-chain split stack (based on #303).
galactic-ipamis now a real CNI IPAM delegation target (github.com/containernetworking/plugins/pkg/ipam.ExecAdd/ExecDel), execed bygalactic-cni/galactic-tap-cni— not the in-process library call step 0 left it as.Explicit IPAM contract
"ipam"block presence in a master plugin's own config. No environment variable can trigger or suppress that anymore — the oldGALACTIC_CNI_ENABLE_LOCAL_IPAM"enforce ipam block" check is gone.ipv6_subnet/ipv4_subnet/address_familiesmove inside the"ipam"block. Their CIDR validation moved with them, intogalactic-ipam's ownparseConf.ipam.typenow names the delegated binary, not a pool-vs-static mode selector. Mode is decided internally:static_ippresence selects static, otherwiseipv6_subnet/ipv4_subnetselect pool mode.enableLocalIPAM→GALACTIC_IPAM_ENABLE_LOCAL_IPAM(internal/config/ipam.go), read only bygalactic-ipamitself, and only fills a default IPv6 pool CIDR when"ipam"is present but under-specified — it can't manufacture the block anymore.The unlock: IPv6 pool persistence
PoolAllocatortracked allocations in memory only — fine for ADD (each pool belongs to exactly one VPCAttachment), useless for DEL (fresh process, nothing to look up). Deallocation used to close that gap by reading the allocated value back out of the BGPAdvertisement CRD annotationgalactic-bgpwrote — the one thing keepinggalactic-ipamcoupled togalactic-bgp's CRD naming and a Kubernetes client it has no other use for.Fixed:
PoolAllocatorgets the same on-disk marker fileIPv4PoolAllocatoralready had (keyed by containerID, flock-guarded, one shared lock dir for both families —ipam.DefaultLockDir). Both allocators gainedDeallocateContainer(containerID)and a peek-onlyLookupContainer(containerID).galactic-ipamnow looks its own allocations up locally — no k8s client, nocrdnames, nobgpv1alpha1import at all.Verification
task lint✅task build✅ (builds + stagesgalactic-ipamtoo)task test:unit✅internal/cniipamat 91% coverage, including a real ADD → CHECK → DEL → CHECK round trip proving the self-contained-by-containerID design holds end to end.task test:e2e— not run, same caveat as refactor(cni): split galactic-cni into veth/tap master plugins #303.🤖 Generated with Claude Code