ip-algo: pod annotation > NodeConfig annotation > random
Build flock Image / build (push) Has been cancelled

Add flock.fritzlab.net/ip-algo as a node-wide default via NodeConfig
metadata.annotations. Pod-level annotation still wins. Empty, missing,
or invalid input at either level falls through to the next; invalid
values warn-log via the agent's slog. Both unset → fully random IID
(unchanged baseline).

ParseAnnotations no longer touches ip-algo; ResolveIPAlgo handles the
full precedence chain, called from PodHandler.Add with the cached
NodeConfig's annotations and the agent logger.

Tests: 9 new TestResolveIPAlgo_* cases covering pod-wins, all
fall-through paths, both-absent, nil node map, whitespace, and
duplicate-as-invalid. Fuzz target rebuilt without ip-algo input space
(now exercised by ResolveIPAlgo unit tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-25 11:09:09 -05:00
parent a6202a36bd
commit c860e9351b
5 changed files with 184 additions and 67 deletions
+90 -17
View File
@@ -174,32 +174,105 @@ func TestParseAnnotations_BoolCaseInsensitive(t *testing.T) {
}
}
func TestParseAnnotations_IPAlgo(t *testing.T) {
a, err := ParseAnnotations(map[string]string{
annotationPrefix + "ip-algo": "namespace,pod,image",
}, BuiltinFamilyDefaults())
if err != nil {
t.Fatal(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)
}
want := []embed.Field{embed.FieldNamespace, embed.FieldPod, embed.FieldImage}
if len(a.IPAlgo) != len(want) {
t.Fatalf("ip-algo len=%d, want %d", len(a.IPAlgo), len(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)
}
for i := range want {
if a.IPAlgo[i] != want[i] {
t.Fatalf("ip-algo[%d]=%s, want %s", i, a.IPAlgo[i], want[i])
}
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 TestParseAnnotations_IPAlgo_Unknown(t *testing.T) {
if _, err := ParseAnnotations(map[string]string{
annotationPrefix + "ip-algo": "namespace,foo",
}, BuiltinFamilyDefaults()); err == nil {
t.Fatalf("expected unknown-field error")
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",