agent: addresses annotation replaces IPAM allocation
Build flock Image / build (push) Successful in 5m27s
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:
@@ -142,22 +142,74 @@ func (r *AnycastReconciler) renderBird(desired map[string]anycastTarget) {
|
||||
return
|
||||
}
|
||||
var v6, v4 []string
|
||||
for ipStr := range desired {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
continue
|
||||
seen := map[string]struct{}{}
|
||||
add := func(ip net.IP) {
|
||||
key := canonical(ip)
|
||||
if _, dup := seen[key]; dup {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
if ip.To4() != nil {
|
||||
v4 = append(v4, ip.To4().String())
|
||||
} else {
|
||||
v6 = append(v6, ip.To16().String())
|
||||
}
|
||||
}
|
||||
for ipStr := range desired {
|
||||
if ip := net.ParseIP(ipStr); ip != nil {
|
||||
add(ip)
|
||||
}
|
||||
}
|
||||
// A pod IP that lives outside the node's BGP aggregate (e.g. an
|
||||
// addresses-annotation IP promoted to be the pod's primary v4 — Plex's
|
||||
// 142.202.202.166 against host004's 172.25.214.0/24) is not naturally
|
||||
// covered by the aggregate, so it must be advertised individually as a
|
||||
// /32 or /128. Anycast and addresses extras are already covered by the
|
||||
// `desired` loop above; this sweep is for promoted-primary IPs which do
|
||||
// not flow through the AnycastReconciler.
|
||||
nodeV6, nodeV4 := parseNodeCIDRs(nc)
|
||||
for _, a := range r.Store.Snapshot() {
|
||||
if a.State != StateCommitted {
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(a.IP6); ip != nil && !ipInAny(ip, nodeV6) {
|
||||
add(ip)
|
||||
}
|
||||
if ip := net.ParseIP(a.IP4); ip != nil && !ipInAny(ip, nodeV4) {
|
||||
add(ip)
|
||||
}
|
||||
}
|
||||
if err := r.Bird.Render(nc, v6, v4, r.RouterID); err != nil {
|
||||
r.Logger.Warn("anycast bird render", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// parseNodeCIDRs parses NodeConfig.Spec.CIDR6/4 strings into IPNets,
|
||||
// silently dropping malformed entries (admission-time validation should
|
||||
// have rejected them long before this point).
|
||||
func parseNodeCIDRs(nc *flockv1alpha1.NodeConfig) (v6, v4 []*net.IPNet) {
|
||||
for _, s := range nc.Spec.CIDR6 {
|
||||
if _, n, err := net.ParseCIDR(s); err == nil {
|
||||
v6 = append(v6, n)
|
||||
}
|
||||
}
|
||||
for _, s := range nc.Spec.CIDR4 {
|
||||
if _, n, err := net.ParseCIDR(s); err == nil {
|
||||
v4 = append(v4, n)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ipInAny(ip net.IP, nets []*net.IPNet) bool {
|
||||
for _, n := range nets {
|
||||
if n.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// installAnycastRoute installs `<ipStr>/<128|32>` pointing at the
|
||||
// nexthop set in t. With one nexthop the route is a plain via-route;
|
||||
// with multiple, it's a multipath route using RTA_MULTIPATH so the
|
||||
|
||||
Reference in New Issue
Block a user