package agent import ( "testing" flockv1alpha1 "code.fritzlab.net/fritzlab/flock/pkg/api/v1alpha1" "code.fritzlab.net/fritzlab/flock/pkg/embed" ) // boolPtr returns a pointer to b — convenient for the *bool pointer fields // in FamilyDefaults where nil means "unset". func boolPtr(b bool) *bool { return &b } func TestBuiltinFamilyDefaults(t *testing.T) { d := BuiltinFamilyDefaults() if !d.WantV6 || !d.WantV4 { t.Fatalf("built-in defaults wrong: v6=%v v4=%v (want dual-stack true/true)", d.WantV6, d.WantV4) } } func TestFamilyDefaultsFromNodeConfig_NilNodeConfig(t *testing.T) { d := FamilyDefaultsFromNodeConfig(nil) if d != BuiltinFamilyDefaults() { t.Fatalf("nil NodeConfig should yield built-in defaults; got %+v", d) } } func TestFamilyDefaultsFromNodeConfig_NilDefaults(t *testing.T) { nc := &flockv1alpha1.NodeConfig{} d := FamilyDefaultsFromNodeConfig(nc) if d != BuiltinFamilyDefaults() { t.Fatalf("missing Defaults should yield built-in; got %+v", d) } } func TestFamilyDefaultsFromNodeConfig_PartialOverride(t *testing.T) { nc := &flockv1alpha1.NodeConfig{ Spec: flockv1alpha1.NodeConfigSpec{ Defaults: &flockv1alpha1.FamilyDefaults{ IPv4: boolPtr(false), }, }, } d := FamilyDefaultsFromNodeConfig(nc) // IPv6 unset → keeps built-in true; IPv4 explicitly set to false → // node opts the family off. Validates that an explicit false beats // the dual-stack baseline rather than being silently overridden. if !d.WantV6 || d.WantV4 { t.Fatalf("partial override wrong: %+v (want v6=true, v4=false)", d) } } func TestFamilyDefaultsFromNodeConfig_FullOverride(t *testing.T) { nc := &flockv1alpha1.NodeConfig{ Spec: flockv1alpha1.NodeConfigSpec{ Defaults: &flockv1alpha1.FamilyDefaults{ IPv6: boolPtr(false), IPv4: boolPtr(true), }, }, } d := FamilyDefaultsFromNodeConfig(nc) if d.WantV6 || !d.WantV4 { t.Fatalf("full override wrong: %+v (want v6=false, v4=true)", d) } } func TestParseAnnotations_BuiltinDefaults(t *testing.T) { // Built-in baseline is dual-stack — no annotation needed. a, err := ParseAnnotations(nil, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if !a.WantV6 || !a.WantV4 { t.Fatalf("expected dual-stack default, got v6=%v v4=%v", a.WantV6, a.WantV4) } } // TestParseAnnotations_OptOutV4 — pods that want IPv6 only must opt out // explicitly via the ipv4 annotation now that the built-in is dual-stack. func TestParseAnnotations_OptOutV4(t *testing.T) { a, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "false", }, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if !a.WantV6 || a.WantV4 { t.Fatalf("ipv4=false override failed: v6=%v v4=%v", a.WantV6, a.WantV4) } } func TestParseAnnotations_NodeDefaultsApplied(t *testing.T) { // Node config says "IPv4 is on by default for this node". d := FamilyDefaults{WantV6: true, WantV4: true} a, err := ParseAnnotations(nil, d) if err != nil { t.Fatal(err) } if !a.WantV6 || !a.WantV4 { t.Fatalf("node defaults not applied: %+v", a) } } func TestParseAnnotations_AnnotationOverridesNodeDefault(t *testing.T) { // Node says dual-stack by default; pod opts out of v4 explicitly. d := FamilyDefaults{WantV6: true, WantV4: true} a, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "false", }, d) if err != nil { t.Fatal(err) } if !a.WantV6 || a.WantV4 { t.Fatalf("annotation override failed: %+v", a) } } func TestParseAnnotations_DualStackViaAnnotation(t *testing.T) { // Same as built-in default; explicit ipv4=true is a no-op now but must // still parse cleanly. a, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "true", }, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if !(a.WantV6 && a.WantV4) { t.Fatalf("expected dual stack, got v6=%v v4=%v", a.WantV6, a.WantV4) } } func TestParseAnnotations_NoFamily(t *testing.T) { // Pod opts out of both families → must be rejected. if _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv6": "false", annotationPrefix + "ipv4": "false", }, BuiltinFamilyDefaults()); err == nil { t.Fatalf("expected error when pod opts out of both families") } } func TestParseAnnotations_NoFamily_NodeDefaultsAlsoOff(t *testing.T) { // Pathological NodeConfig that disables both families. Even with no pod // annotation we must reject — otherwise a pod gets an empty allocation. d := FamilyDefaults{WantV6: false, WantV4: false} if _, err := ParseAnnotations(nil, d); err == nil { t.Fatalf("expected error when both defaults are false") } } func TestParseAnnotations_BoolStrictness(t *testing.T) { // Common misuses that should be rejected so typos don't silently flip // behaviour to the implicit-false default. bad := []string{"1", "0", "yes", "no", "TrueFalse", " "} for _, v := range bad { _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": v, }, BuiltinFamilyDefaults()) if err == nil { t.Errorf("expected error for ipv4=%q", v) } } } func TestParseAnnotations_BoolCaseInsensitive(t *testing.T) { for _, v := range []string{"TRUE", "True", " true ", "FALSE", "False"} { _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": v, }, BuiltinFamilyDefaults()) if err != nil { t.Errorf("expected ipv4=%q to parse cleanly: %v", v, err) } } } // ResolveIPAlgo: precedence is pod → node → nil. Empty / missing / invalid // at any level falls through to the next under the relaxed user-defined rule // "all three mean unset". func TestResolveIPAlgo_PodWins(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: "namespace,app"} node := map[string]string{annotationPrefix + annIPAlgo: "image"} got := ResolveIPAlgo(pod, node, nil) want := []embed.Field{embed.FieldNamespace, embed.FieldApp} if !equalFields(got, want) { t.Fatalf("got %v, want %v", got, want) } } func TestResolveIPAlgo_PodAbsentFallsToNode(t *testing.T) { node := map[string]string{annotationPrefix + annIPAlgo: "image"} got := ResolveIPAlgo(nil, node, nil) want := []embed.Field{embed.FieldImage} if !equalFields(got, want) { t.Fatalf("got %v, want %v", got, want) } } func TestResolveIPAlgo_PodEmptyFallsToNode(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: ""} node := map[string]string{annotationPrefix + annIPAlgo: "image"} got := ResolveIPAlgo(pod, node, nil) want := []embed.Field{embed.FieldImage} if !equalFields(got, want) { t.Fatalf("got %v, want %v", got, want) } } func TestResolveIPAlgo_PodInvalidFallsToNode(t *testing.T) { for _, podVal := range []string{"namespace,bogus", "ns", ",", "namespace,namespace"} { pod := map[string]string{annotationPrefix + annIPAlgo: podVal} node := map[string]string{annotationPrefix + annIPAlgo: "app"} got := ResolveIPAlgo(pod, node, nil) want := []embed.Field{embed.FieldApp} if !equalFields(got, want) { t.Fatalf("podVal=%q: got %v, want %v", podVal, got, want) } } } func TestResolveIPAlgo_BothInvalidReturnsNil(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: "bogus"} node := map[string]string{annotationPrefix + annIPAlgo: "also-bogus"} if got := ResolveIPAlgo(pod, node, nil); got != nil { t.Fatalf("got %v, want nil", got) } } func TestResolveIPAlgo_BothAbsentReturnsNil(t *testing.T) { if got := ResolveIPAlgo(nil, nil, nil); got != nil { t.Fatalf("got %v, want nil", got) } } func TestResolveIPAlgo_NilNodeMap(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: "image"} got := ResolveIPAlgo(pod, nil, nil) want := []embed.Field{embed.FieldImage} if !equalFields(got, want) { t.Fatalf("got %v, want %v", got, want) } } func TestResolveIPAlgo_Whitespace(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: " namespace , app "} got := ResolveIPAlgo(pod, nil, nil) want := []embed.Field{embed.FieldNamespace, embed.FieldApp} if !equalFields(got, want) { t.Fatalf("got %v, want %v", got, want) } } func TestResolveIPAlgo_DuplicateInvalidates(t *testing.T) { pod := map[string]string{annotationPrefix + annIPAlgo: "app,app"} node := map[string]string{annotationPrefix + annIPAlgo: "namespace"} got := ResolveIPAlgo(pod, node, nil) want := []embed.Field{embed.FieldNamespace} if !equalFields(got, want) { t.Fatalf("got %v, want %v (duplicate must collapse to invalid)", got, want) } } func equalFields(a, b []embed.Field) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestParseAnnotations_CIDR(t *testing.T) { a, err := ParseAnnotations(map[string]string{ annotationPrefix + "cidr6": "2602:817:3000:f001::/64, 2602:817:3000:f002::/64", }, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if len(a.CIDR6) != 2 { t.Fatalf("cidr6 len=%d", len(a.CIDR6)) } } func TestParseAnnotations_CIDR_FamilyMismatch(t *testing.T) { // v4 prefix in a cidr6 annotation must not silently slip through. if _, err := ParseAnnotations(map[string]string{ annotationPrefix + "cidr6": "10.0.0.0/8", }, BuiltinFamilyDefaults()); err == nil { t.Fatalf("expected family mismatch error") } if _, err := ParseAnnotations(map[string]string{ annotationPrefix + "cidr4": "2602:817::/32", }, BuiltinFamilyDefaults()); err == nil { t.Fatalf("expected family mismatch error") } } func TestParseAnnotations_Anycast_Mixed(t *testing.T) { // Anycast accepts both families together — typical for a service that // advertises one v6 and one v4 anycast IP. a, err := ParseAnnotations(map[string]string{ annotationPrefix + "anycast": "2602:817:3000:ac::1, 172.25.255.1", }, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if len(a.Anycast) != 2 { t.Fatalf("anycast len=%d", len(a.Anycast)) } } func TestParseAnnotations_Addresses_Mixed(t *testing.T) { // Plex's case: one v6 and one v4 supplied via addresses, both families // enabled (built-in defaults). Both IPs are recorded; conflict check // passes; later in handlers.Add they get peeled into primary slots. a, err := ParseAnnotations(map[string]string{ annotationPrefix + "addresses": "2602:817:3000:c606::166, 142.202.202.166", }, BuiltinFamilyDefaults()) if err != nil { t.Fatal(err) } if len(a.Addresses) != 2 { t.Fatalf("addresses len=%d", len(a.Addresses)) } } func TestParseAnnotations_Addresses_ConflictV4Disabled(t *testing.T) { // addresses contains a v4 but the pod has explicitly opted out of v4. // The IP would land on eth0 with no default v4 route, so reject at ADD. _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "false", annotationPrefix + "addresses": "142.202.202.166", }, BuiltinFamilyDefaults()) if err == nil { t.Fatal("want error for ipv4=false + addresses v4, got nil") } } func TestParseAnnotations_Addresses_ConflictV6Disabled(t *testing.T) { _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv6": "false", annotationPrefix + "ipv4": "true", annotationPrefix + "addresses": "2602:817:3000:c606::166", }, BuiltinFamilyDefaults()) if err == nil { t.Fatal("want error for ipv6=false + addresses v6, got nil") } } func TestParseAnnotations_Anycast_ConflictV4Disabled(t *testing.T) { // Anycast on lo also requires the family enabled — replies need the // in-pod default v4 route off eth0, which only exists when v4 is on. _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "false", annotationPrefix + "anycast": "172.25.255.1", }, BuiltinFamilyDefaults()) if err == nil { t.Fatal("want error for ipv4=false + anycast v4, got nil") } } func TestParseAnnotations_Anycast_ConflictV6Disabled(t *testing.T) { _, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv6": "false", annotationPrefix + "ipv4": "true", annotationPrefix + "anycast": "2602:817:3000:ac::1", }, BuiltinFamilyDefaults()) if err == nil { t.Fatal("want error for ipv6=false + anycast v6, got nil") } } func TestParseAnnotations_Addresses_NodeDefaultV4Off(t *testing.T) { // NodeConfig default opts v4 off for the node, and the pod has no // explicit ipv4 annotation. addresses-v4 still conflicts because the // resolved WantV4 is false. Operator must add `ipv4: "true"` on the // pod to override the node default. defaults := FamilyDefaults{WantV6: true, WantV4: false} _, err := ParseAnnotations(map[string]string{ annotationPrefix + "addresses": "142.202.202.166", }, defaults) if err == nil { t.Fatal("want error for NodeConfig v4=false + addresses v4, got nil") } } func TestParseAnnotations_Addresses_NodeDefaultV4Off_PodOptsBackIn(t *testing.T) { // Same as above but pod explicitly sets ipv4=true to override the node // default. Conflict resolved; parse succeeds. defaults := FamilyDefaults{WantV6: true, WantV4: false} a, err := ParseAnnotations(map[string]string{ annotationPrefix + "ipv4": "true", annotationPrefix + "addresses": "142.202.202.166", }, defaults) if err != nil { t.Fatalf("expected ok, got %v", err) } if !a.WantV4 || len(a.Addresses) != 1 { t.Fatalf("unexpected: %+v", a) } } func TestParseCNIArgs(t *testing.T) { args := ParseCNIArgs("IgnoreUnknown=1;K8S_POD_NAMESPACE=mail;K8S_POD_NAME=stalwart-0;K8S_POD_INFRA_CONTAINER_ID=abc123") if args.PodNamespace != "mail" || args.PodName != "stalwart-0" || args.InfraID != "abc123" { t.Fatalf("ParseCNIArgs got %+v", args) } } func TestParseCNIArgs_EmptyAndMalformed(t *testing.T) { // Permissive: malformed entries are skipped, never crash. a := ParseCNIArgs("") if a.PodName != "" { t.Fatalf("empty input should yield empty CNIArgs, got %+v", a) } a = ParseCNIArgs(";;K8S_POD_NAMESPACE=ns;noequalshere;=novalue;K8S_POD_NAME=p") if a.PodNamespace != "ns" || a.PodName != "p" { t.Fatalf("permissive parse failed: %+v", a) } }