a17d33e182
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>
187 lines
5.4 KiB
Go
187 lines
5.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
func ptrBool(b bool) *bool { return &b }
|
|
|
|
func mkPod(name string, owner *metav1.OwnerReference, labels map[string]string, image string) *corev1.Pod {
|
|
p := &corev1.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels},
|
|
}
|
|
if owner != nil {
|
|
p.OwnerReferences = []metav1.OwnerReference{*owner}
|
|
}
|
|
if image != "" {
|
|
p.Spec.Containers = []corev1.Container{{Image: image}}
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestDeriveAppName_DeploymentReplicaSet(t *testing.T) {
|
|
owner := &metav1.OwnerReference{
|
|
Kind: "ReplicaSet",
|
|
Name: "traefik-789df685f",
|
|
Controller: ptrBool(true),
|
|
}
|
|
pod := mkPod("traefik-789df685f-hqvfl", owner,
|
|
map[string]string{podTemplateHashLabel: "789df685f"}, "")
|
|
if got := deriveAppName(pod); got != "traefik" {
|
|
t.Fatalf("got %q, want %q", got, "traefik")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAppName_StatefulSet(t *testing.T) {
|
|
owner := &metav1.OwnerReference{
|
|
Kind: "StatefulSet",
|
|
Name: "gitea",
|
|
Controller: ptrBool(true),
|
|
}
|
|
pod := mkPod("gitea-0", owner, nil, "")
|
|
if got := deriveAppName(pod); got != "gitea" {
|
|
t.Fatalf("got %q, want %q", got, "gitea")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAppName_DaemonSet(t *testing.T) {
|
|
owner := &metav1.OwnerReference{
|
|
Kind: "DaemonSet",
|
|
Name: "flock-agent",
|
|
Controller: ptrBool(true),
|
|
}
|
|
pod := mkPod("flock-agent-abcde", owner, nil, "")
|
|
if got := deriveAppName(pod); got != "flock-agent" {
|
|
t.Fatalf("got %q, want %q", got, "flock-agent")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAppName_BarePod(t *testing.T) {
|
|
pod := mkPod("standalone", nil, nil, "")
|
|
if got := deriveAppName(pod); got != "standalone" {
|
|
t.Fatalf("got %q, want %q", got, "standalone")
|
|
}
|
|
}
|
|
|
|
// TestDeriveAppName_RSWithoutTemplateHash — ReplicaSet owners that don't
|
|
// follow the standard "<deploy>-<hash>" naming convention (e.g. a custom
|
|
// controller) keep the RS name as-is. All replicas of that RS still align,
|
|
// which is the second-best correctness offer.
|
|
func TestDeriveAppName_RSWithoutTemplateHash(t *testing.T) {
|
|
owner := &metav1.OwnerReference{
|
|
Kind: "ReplicaSet",
|
|
Name: "weird-rs-name",
|
|
Controller: ptrBool(true),
|
|
}
|
|
pod := mkPod("weird-rs-name-xyz", owner, nil, "")
|
|
if got := deriveAppName(pod); got != "weird-rs-name" {
|
|
t.Fatalf("got %q, want %q", got, "weird-rs-name")
|
|
}
|
|
}
|
|
|
|
func TestDeriveAppName_NonControllerOwnerIgnored(t *testing.T) {
|
|
// OwnerReference without Controller=true must be ignored — only the
|
|
// controller owner is the canonical workload.
|
|
owner := &metav1.OwnerReference{
|
|
Kind: "Foo",
|
|
Name: "irrelevant",
|
|
// Controller pointer left nil.
|
|
}
|
|
pod := mkPod("solo", owner, nil, "")
|
|
if got := deriveAppName(pod); got != "solo" {
|
|
t.Fatalf("got %q, want %q", got, "solo")
|
|
}
|
|
}
|
|
|
|
func TestPodImageRef(t *testing.T) {
|
|
pod := mkPod("p", nil, nil, "traefik:v3.5")
|
|
if got := podImageRef(pod); got != "traefik:v3.5" {
|
|
t.Fatalf("got %q, want %q", got, "traefik:v3.5")
|
|
}
|
|
empty := mkPod("p", nil, nil, "")
|
|
if got := podImageRef(empty); got != "" {
|
|
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)
|
|
}
|
|
}
|