65b2fb5b17
Build flock Image / build (push) Has been cancelled
The `pod` field hashed pod.Name, which differs per replica because of
the ReplicaSet pod-template-hash + 5-char random suffix. With
namespace,pod,image, all replicas of the same Deployment got distinct
hextets even though they were the same workload.
Replace `pod` with `app` — a stable workload identifier derived from
the controller chain:
- Deployment → ReplicaSet → Pod: strip the pod-template-hash suffix
from the RS name (`traefik-789df685f` → `traefik`).
- StatefulSet/DaemonSet/Job → Pod: use controller name as-is.
- Bare pod: pod name.
Image now comes from pod.Spec.Containers[0].Image (the spec'd
reference). 64-hex-char values are treated as sha256 digests and
parsed as before; everything else (image:tag, short SHA) is FNV-1a-64'd
as a string. This makes `traefik:v3.5` deterministic across replicas
without needing the runtime-resolved digest.
Net effect: namespace,app,image yields identical hextets across all
replicas of the same Deployment except the trailing random N nibble.
embed.Values.Pod → App; AllocRequest.Pod kept for log context only,
new App and Image fields drive the embed call. handlers.go computes
both via deriveAppName + podImageRef helpers.
Tests: 7 new TestDeriveAppName_* cases (Deploy/STS/DS/bare/RS-without-
hash/non-controller-owner) + TestPodImageRef. Existing fuzz seeds
updated for the new keyword.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
334 lines
10 KiB
Go
334 lines
10 KiB
Go
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 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)
|
|
}
|
|
}
|