a6202a36bd
Build flock Image / build (push) Has been cancelled
BuiltinFamilyDefaults() now returns {WantV6: true, WantV4: true}. Pods
that want a single family explicitly opt out via the
flock.fritzlab.net/ipv4 (or ipv6) annotation, or the operator narrows
the default at the node level via NodeConfig.Spec.Defaults.
Annotation precedence is unchanged: pod annotation > NodeConfig defaults
> built-in baseline. Tests updated to reflect the new baseline; the
"opt out of v4" path now has explicit coverage.
Docs updated:
- NodeConfig.Spec.Defaults Go doc + CRD descriptions reflect the new
baseline and its overrides
- README opening framing softened from "IPv6-first" to "dual-stack,
IPv6-friendly"; example pods + spec.defaults table flipped to
treat dual-stack as the default and v6/v4-only as overrides
- README NetworkPolicy line in the comparison table flipped to
"yes (nftables)" since v1 enforcement shipped
- Limitations note about IPv4-only destinations rewritten — every
pod has v4 by default now, so the question is whether your IPv4
pool is routable beyond your network
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
261 lines
7.9 KiB
Go
261 lines
7.9 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
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))
|
|
}
|
|
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 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 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)
|
|
}
|
|
}
|