Files
flock/pkg/agent/annotations_test.go
T

334 lines
10 KiB
Go
Raw Normal View History

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,pod"}
node := map[string]string{annotationPrefix + annIPAlgo: "image"}
got := ResolveIPAlgo(pod, node, nil)
want := []embed.Field{embed.FieldNamespace, embed.FieldPod}
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: "pod"}
got := ResolveIPAlgo(pod, node, nil)
want := []embed.Field{embed.FieldPod}
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 , pod "}
got := ResolveIPAlgo(pod, nil, nil)
want := []embed.Field{embed.FieldNamespace, embed.FieldPod}
if !equalFields(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
func TestResolveIPAlgo_DuplicateInvalidates(t *testing.T) {
pod := map[string]string{annotationPrefix + annIPAlgo: "pod,pod"}
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 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)
}
}