2026-04-24 22:33:48 -05:00
|
|
|
package agent
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
2026-04-25 09:25:45 -05:00
|
|
|
flockv1alpha1 "code.fritzlab.net/fritzlab/flock/pkg/api/v1alpha1"
|
2026-04-24 22:33:48 -05:00
|
|
|
"code.fritzlab.net/fritzlab/flock/pkg/embed"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-25 09:25:45 -05:00
|
|
|
// 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()
|
2026-04-25 10:07:48 -05:00
|
|
|
if !d.WantV6 || !d.WantV4 {
|
|
|
|
|
t.Fatalf("built-in defaults wrong: v6=%v v4=%v (want dual-stack true/true)", d.WantV6, d.WantV4)
|
2026-04-25 09:25:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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{
|
2026-04-25 10:07:48 -05:00
|
|
|
IPv4: boolPtr(false),
|
2026-04-25 09:25:45 -05:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
d := FamilyDefaultsFromNodeConfig(nc)
|
2026-04-25 10:07:48 -05:00
|
|
|
// 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)
|
2026-04-25 09:25:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-04-25 10:07:48 -05:00
|
|
|
// Built-in baseline is dual-stack — no annotation needed.
|
2026-04-25 09:25:45 -05:00
|
|
|
a, err := ParseAnnotations(nil, BuiltinFamilyDefaults())
|
2026-04-24 22:33:48 -05:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-04-25 10:07:48 -05:00
|
|
|
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)
|
|
|
|
|
}
|
2026-04-24 22:33:48 -05:00
|
|
|
if !a.WantV6 || a.WantV4 {
|
2026-04-25 10:07:48 -05:00
|
|
|
t.Fatalf("ipv4=false override failed: v6=%v v4=%v", a.WantV6, a.WantV4)
|
2026-04-24 22:33:48 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 09:25:45 -05:00
|
|
|
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) {
|
2026-04-25 10:07:48 -05:00
|
|
|
// Same as built-in default; explicit ipv4=true is a no-op now but must
|
|
|
|
|
// still parse cleanly.
|
2026-04-24 22:33:48 -05:00
|
|
|
a, err := ParseAnnotations(map[string]string{
|
|
|
|
|
annotationPrefix + "ipv4": "true",
|
2026-04-25 09:25:45 -05:00
|
|
|
}, BuiltinFamilyDefaults())
|
2026-04-24 22:33:48 -05:00
|
|
|
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) {
|
2026-04-25 10:07:48 -05:00
|
|
|
// Pod opts out of both families → must be rejected.
|
2026-04-24 22:33:48 -05:00
|
|
|
if _, err := ParseAnnotations(map[string]string{
|
|
|
|
|
annotationPrefix + "ipv6": "false",
|
2026-04-25 10:07:48 -05:00
|
|
|
annotationPrefix + "ipv4": "false",
|
2026-04-25 09:25:45 -05:00
|
|
|
}, BuiltinFamilyDefaults()); err == nil {
|
2026-04-25 10:07:48 -05:00
|
|
|
t.Fatalf("expected error when pod opts out of both families")
|
2026-04-24 22:33:48 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 09:25:45 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 11:09:09 -05:00
|
|
|
// 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) {
|
2026-04-25 11:42:06 -05:00
|
|
|
pod := map[string]string{annotationPrefix + annIPAlgo: "namespace,app"}
|
2026-04-25 11:09:09 -05:00
|
|
|
node := map[string]string{annotationPrefix + annIPAlgo: "image"}
|
|
|
|
|
got := ResolveIPAlgo(pod, node, nil)
|
2026-04-25 11:42:06 -05:00
|
|
|
want := []embed.Field{embed.FieldNamespace, embed.FieldApp}
|
2026-04-25 11:09:09 -05:00
|
|
|
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)
|
2026-04-24 22:33:48 -05:00
|
|
|
}
|
2026-04-25 11:09:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
2026-04-24 22:33:48 -05:00
|
|
|
}
|
2026-04-25 11:09:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveIPAlgo_PodInvalidFallsToNode(t *testing.T) {
|
|
|
|
|
for _, podVal := range []string{"namespace,bogus", "ns", ",", "namespace,namespace"} {
|
|
|
|
|
pod := map[string]string{annotationPrefix + annIPAlgo: podVal}
|
2026-04-25 11:42:06 -05:00
|
|
|
node := map[string]string{annotationPrefix + annIPAlgo: "app"}
|
2026-04-25 11:09:09 -05:00
|
|
|
got := ResolveIPAlgo(pod, node, nil)
|
2026-04-25 11:42:06 -05:00
|
|
|
want := []embed.Field{embed.FieldApp}
|
2026-04-25 11:09:09 -05:00
|
|
|
if !equalFields(got, want) {
|
|
|
|
|
t.Fatalf("podVal=%q: got %v, want %v", podVal, got, want)
|
2026-04-24 22:33:48 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 11:09:09 -05:00
|
|
|
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) {
|
2026-04-25 11:42:06 -05:00
|
|
|
pod := map[string]string{annotationPrefix + annIPAlgo: " namespace , app "}
|
2026-04-25 11:09:09 -05:00
|
|
|
got := ResolveIPAlgo(pod, nil, nil)
|
2026-04-25 11:42:06 -05:00
|
|
|
want := []embed.Field{embed.FieldNamespace, embed.FieldApp}
|
2026-04-25 11:09:09 -05:00
|
|
|
if !equalFields(got, want) {
|
|
|
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestResolveIPAlgo_DuplicateInvalidates(t *testing.T) {
|
2026-04-25 11:42:06 -05:00
|
|
|
pod := map[string]string{annotationPrefix + annIPAlgo: "app,app"}
|
2026-04-25 11:09:09 -05:00
|
|
|
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
|
|
|
|
|
}
|
2026-04-25 09:25:45 -05:00
|
|
|
}
|
2026-04-25 11:09:09 -05:00
|
|
|
return true
|
2026-04-25 09:25:45 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-24 22:33:48 -05:00
|
|
|
func TestParseAnnotations_CIDR(t *testing.T) {
|
|
|
|
|
a, err := ParseAnnotations(map[string]string{
|
|
|
|
|
annotationPrefix + "cidr6": "2602:817:3000:f001::/64, 2602:817:3000:f002::/64",
|
2026-04-25 09:25:45 -05:00
|
|
|
}, BuiltinFamilyDefaults())
|
2026-04-24 22:33:48 -05:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(a.CIDR6) != 2 {
|
|
|
|
|
t.Fatalf("cidr6 len=%d", len(a.CIDR6))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 09:25:45 -05:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 09:46:48 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 22:33:48 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-25 09:25:45 -05:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|