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
+72 -27
View File
@@ -2,6 +2,7 @@ package agent
import (
"fmt"
"log/slog"
"net"
"strings"
@@ -87,9 +88,6 @@ type ParsedAnnotations struct {
CIDR6 []*net.IPNet
// CIDR4 narrows IPv4 allocation. nil/empty means "use any node CIDR4".
CIDR4 []*net.IPNet
// IPAlgo is the ordered list of identity fields used to build the IID.
// nil/empty means "random IID".
IPAlgo []embed.Field
// Anycast is the set of anycast IPs to bind on the pod's loopback.
// nil/empty means "no anycast".
Anycast []net.IP
@@ -144,14 +142,6 @@ func ParseAnnotations(in map[string]string, defaults FamilyDefaults) (*ParsedAnn
out.CIDR4 = nets
}
if v, ok := in[annotationPrefix+annIPAlgo]; ok {
fields, err := parseIPAlgo(v)
if err != nil {
return nil, fmt.Errorf("annotation %s: %w", annIPAlgo, err)
}
out.IPAlgo = fields
}
if v, ok := in[annotationPrefix+annAnycast]; ok {
ips, err := parseIPList(v)
if err != nil {
@@ -247,30 +237,85 @@ func parseIPList(s string) ([]net.IP, error) {
return out, nil
}
// parseIPAlgo parses the ip-algo annotation. Each comma-separated token must
// match one of: namespace, pod, image. Empty tokens are dropped; unknown
// tokens are reported.
func parseIPAlgo(s string) ([]embed.Field, error) {
// ResolveIPAlgo resolves the effective ip-algo for a pod. Precedence:
//
// pod annotation → NodeConfig annotation → nil (random IID).
//
// Empty, missing, or invalid annotations at any level fall through to the
// next. Invalid input emits a warning via log; a nil log is silent. A nil
// return value means "no algo, generate a fully random IID".
//
// "Invalid" is everything tryParseIPAlgo cannot turn into a non-empty,
// duplicate-free subset of {namespace, pod, image} — unrecognised tokens,
// duplicates, lists that resolve to zero fields after trimming.
func ResolveIPAlgo(podAnn, nodeAnn map[string]string, log *slog.Logger) []embed.Field {
if v, ok := podAnn[annotationPrefix+annIPAlgo]; ok {
if fields := tryParseIPAlgo(v); fields != nil {
return fields
}
warnIPAlgo(log, "pod", v)
}
if v, ok := nodeAnn[annotationPrefix+annIPAlgo]; ok {
if fields := tryParseIPAlgo(v); fields != nil {
return fields
}
warnIPAlgo(log, "NodeConfig", v)
}
return nil
}
// warnIPAlgo logs a single warning when an ip-algo annotation is present
// but cannot be parsed. Empty values are not worth a warn — they are
// indistinguishable from "key absent" by the user's design rule, so we
// only warn when a non-empty value failed parsing.
func warnIPAlgo(log *slog.Logger, source, value string) {
if log == nil {
return
}
if strings.TrimSpace(value) == "" {
return
}
log.Warn("ignoring invalid ip-algo annotation; falling through",
"source", source, "value", value)
}
// tryParseIPAlgo parses an ip-algo annotation value under the relaxed
// "invalid → unset" rules. Returns nil for: empty input, unrecognised
// tokens, duplicate fields, or anything that resolves to zero fields after
// trimming. Returns the ordered field list otherwise.
//
// Duplicates collapse to nil rather than dedup-and-keep so the operator
// notices their malformed annotation via the warn log instead of silently
// losing a field they thought they had specified.
func tryParseIPAlgo(s string) []embed.Field {
var out []embed.Field
seen := map[embed.Field]struct{}{}
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
switch part {
case "":
if part == "" {
continue
case string(embed.FieldNamespace):
out = append(out, embed.FieldNamespace)
case string(embed.FieldPod):
out = append(out, embed.FieldPod)
case string(embed.FieldImage):
out = append(out, embed.FieldImage)
default:
return nil, fmt.Errorf("unknown ip-algo field %q (allowed: namespace, pod, image)", part)
}
var f embed.Field
switch part {
case string(embed.FieldNamespace):
f = embed.FieldNamespace
case string(embed.FieldPod):
f = embed.FieldPod
case string(embed.FieldImage):
f = embed.FieldImage
default:
return nil
}
if _, dup := seen[f]; dup {
return nil
}
seen[f] = struct{}{}
out = append(out, f)
}
if len(out) == 0 {
return nil, fmt.Errorf("empty ip-algo")
return nil
}
return out, nil
return out
}
// CNIArgs is the typed view of the K=V;K=V CNI_ARGS string passed by kubelet.