agent: addresses annotation replaces IPAM allocation
Build flock Image / build (push) Successful in 5m27s

When flock.fritzlab.net/addresses provides a v6 or v4, the IP becomes
the pod's primary IP for that family — bound to eth0, default route off
it, on-link host route via setHostRoute, and a per-pod /128 or /32 in
BGP. IPAM no longer allocates a private IP alongside it. The pod ends up
with exactly the operator-supplied addresses on eth0 (plus any extras
beyond the first-of-family, which keep the pre-existing layered
behavior).

This is the fix the original addresses-annotation work missed: bug #1
allocated a private IP next to the public one (so VPN-routed clients
could land on the private path on Plex). Promoting addresses-supplied
IPs into the IPAM-style routing slot keeps the public IP as the only
primary IP visible from outside.

Three pieces:
- annotations.go: reject pods whose addresses/anycast IP family is
  disabled (ipv6/ipv4 annotation or NodeConfig default). Both annotation
  types rely on the family being enabled for return-path routing.
- handlers.go: peel first v6 + first v4 from Addresses into res.IP6/IP4;
  suppress IPAM for those families; skip IPAM call entirely if both
  families are addresses-supplied.
- anycast_linux.go: extend renderBird to advertise any IPAM IP that's
  outside the node's BGP aggregate as a per-pod /32 or /128. This is
  what makes 142.202.202.166 reachable when host004's pod CIDR is
  172.25.214.0/24 — the addresses-promoted IP isn't covered by the
  aggregate.

Tests: 7 new annotation tests covering the conflict cases (ipv4=false +
addresses-v4, NodeConfig default + addresses-v4, etc.) plus 5 unit tests
for the splitAddressesPrimary helper.

README updated with the addresses-replaces-IPAM behavior, the
addresses-vs-anycast comparison, the conflict rule, and a Plex-style
example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-29 09:46:48 -05:00
parent 40e13037b5
commit a17d33e182
6 changed files with 376 additions and 11 deletions
+78
View File
@@ -1,6 +1,7 @@
package agent
import (
"net"
"testing"
corev1 "k8s.io/api/core/v1"
@@ -106,3 +107,80 @@ func TestPodImageRef(t *testing.T) {
t.Fatalf("got %q, want \"\"", got)
}
}
func TestSplitAddressesPrimary_BothFamilies(t *testing.T) {
// Plex pattern: one v6 + one v4 → both peel out, no extras.
ips := []net.IP{
net.ParseIP("2602:817:3000:c606::166"),
net.ParseIP("142.202.202.166"),
}
v6, v4, extras := splitAddressesPrimary(ips)
if v6 == nil || v6.String() != "2602:817:3000:c606::166" {
t.Fatalf("v6 = %v", v6)
}
if v4 == nil || v4.String() != "142.202.202.166" {
t.Fatalf("v4 = %v", v4)
}
if len(extras) != 0 {
t.Fatalf("extras = %v, want empty", extras)
}
}
func TestSplitAddressesPrimary_OnlyV4(t *testing.T) {
v6, v4, extras := splitAddressesPrimary([]net.IP{net.ParseIP("142.202.202.166")})
if v6 != nil {
t.Fatalf("v6 should be nil, got %v", v6)
}
if v4 == nil || v4.String() != "142.202.202.166" {
t.Fatalf("v4 = %v", v4)
}
if len(extras) != 0 {
t.Fatalf("extras = %v", extras)
}
}
func TestSplitAddressesPrimary_OnlyV6(t *testing.T) {
v6, v4, extras := splitAddressesPrimary([]net.IP{net.ParseIP("2602:817:3000:c606::166")})
if v4 != nil {
t.Fatalf("v4 should be nil, got %v", v4)
}
if v6 == nil || v6.String() != "2602:817:3000:c606::166" {
t.Fatalf("v6 = %v", v6)
}
if len(extras) != 0 {
t.Fatalf("extras = %v", extras)
}
}
func TestSplitAddressesPrimary_Empty(t *testing.T) {
v6, v4, extras := splitAddressesPrimary(nil)
if v6 != nil || v4 != nil || extras != nil {
t.Fatalf("nil input should yield nil outputs, got v6=%v v4=%v extras=%v", v6, v4, extras)
}
}
func TestSplitAddressesPrimary_Extras(t *testing.T) {
// Multiple v4s — only the first peels into the primary slot; the rest
// stay in extras for layered-eth0 binding via the AnycastReconciler.
// (Not a current production use case, but the code should handle it
// without dropping IPs.)
ips := []net.IP{
net.ParseIP("142.202.202.166"),
net.ParseIP("2602:817:3000:c606::166"),
net.ParseIP("142.202.202.167"),
net.ParseIP("2602:817:3000:c606::167"),
}
v6, v4, extras := splitAddressesPrimary(ips)
if v4.String() != "142.202.202.166" {
t.Fatalf("v4 primary = %v, want 142.202.202.166", v4)
}
if v6.String() != "2602:817:3000:c606::166" {
t.Fatalf("v6 primary = %v, want 2602:817:3000:c606::166", v6)
}
if len(extras) != 2 {
t.Fatalf("extras len = %d, want 2", len(extras))
}
if extras[0].String() != "142.202.202.167" || extras[1].String() != "2602:817:3000:c606::167" {
t.Fatalf("extras order/content wrong: %v", extras)
}
}