ip-algo: pod annotation > NodeConfig annotation > random
Build flock Image / build (push) Has been cancelled
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:
+72
-27
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user