20f47916af
Build flock Image / build (push) Has been cancelled
- cmd/flock + cmd/flock-agent: build cleanly; CNI ADD/DEL/CHECK return ErrInternal stubs until M2; agent boots, opens unix socket, logs JSON. - pkg/agent/state.go: durable allocations.json (atomic write + fsync + parent fsync); pending/committed lifecycle. Tests cover round-trip, replace-by-cid, version mismatch, no-leak-on-tmp. - pkg/embed/suffix.go: ip-algo IID embedding. Tests cover the /48-/96 nibble distribution table from the design doc, determinism, prefix preservation, N-nibble isolation, digest-vs-fallback divergence. - pkg/api/v1alpha1: minimal NodeConfig types (no controller-runtime yet). - deploy/: NodeConfig CRD, empty ServiceAccount/ClusterRole, DaemonSet pinned to flock.fritzlab.net/agent="" label so it only runs on opted-in nodes. - .gitea/workflows/main.yaml + Dockerfile: build + push to code.fritzlab.net/fritzlab/flock; runs go test in CI. Design doc: dfritzlab/k8s-manager/dfritz-cni.md. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
package embed
|
|
|
|
import (
|
|
"net"
|
|
"testing"
|
|
)
|
|
|
|
func mustCIDR(t *testing.T, s string) *net.IPNet {
|
|
t.Helper()
|
|
_, n, err := net.ParseCIDR(s)
|
|
if err != nil {
|
|
t.Fatalf("ParseCIDR(%q): %v", s, err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestDistribute(t *testing.T) {
|
|
cases := []struct {
|
|
total, k int
|
|
want []int
|
|
}{
|
|
// from the doc table
|
|
{19, 1, []int{19}}, // /48 1 field — would exceed MaxFieldNibbles, see error test below
|
|
{19, 2, []int{10, 9}},
|
|
{19, 3, []int{7, 6, 6}},
|
|
{17, 1, []int{17}},
|
|
{17, 2, []int{9, 8}},
|
|
{17, 3, []int{6, 6, 5}},
|
|
{15, 1, []int{15}},
|
|
{15, 2, []int{8, 7}},
|
|
{15, 3, []int{5, 5, 5}},
|
|
{11, 1, []int{11}},
|
|
{11, 2, []int{6, 5}},
|
|
{11, 3, []int{4, 4, 3}},
|
|
{7, 1, []int{7}},
|
|
{7, 2, []int{4, 3}},
|
|
{7, 3, []int{3, 2, 2}},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := distribute(c.total, c.k)
|
|
if c.total > MaxFieldNibbles && c.k == 1 {
|
|
if err == nil {
|
|
t.Errorf("distribute(%d,%d): expected MaxFieldNibbles error", c.total, c.k)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("distribute(%d,%d): %v", c.total, c.k, err)
|
|
continue
|
|
}
|
|
if !equal(got, c.want) {
|
|
t.Errorf("distribute(%d,%d) = %v, want %v", c.total, c.k, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func equal(a, b []int) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestEmbed_Slash64Deterministic(t *testing.T) {
|
|
// /64 with 3 fields: 5+5+5+1 nibbles = 64-bit IID.
|
|
net64 := mustCIDR(t, "2602:817:3000:f001::/64")
|
|
addr, err := Embed(net64,
|
|
[]Field{FieldNamespace, FieldPod, FieldImage},
|
|
Values{Namespace: "mail", Pod: "stalwart-0", ImageFallback: "container-abc"},
|
|
0xe,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Embed: %v", err)
|
|
}
|
|
// Property: same inputs → same output (twice).
|
|
addr2, err := Embed(net64,
|
|
[]Field{FieldNamespace, FieldPod, FieldImage},
|
|
Values{Namespace: "mail", Pod: "stalwart-0", ImageFallback: "container-abc"},
|
|
0xe,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !addr.Equal(addr2) {
|
|
t.Fatalf("non-deterministic: %s vs %s", addr, addr2)
|
|
}
|
|
// Property: prefix preserved.
|
|
if !net64.Contains(addr) {
|
|
t.Fatalf("addr %s outside network %s", addr, net64)
|
|
}
|
|
// Property: last nibble is exactly N.
|
|
if got := addr[len(addr)-1] & 0x0F; got != 0xe {
|
|
t.Fatalf("last nibble = %x, want e", got)
|
|
}
|
|
}
|
|
|
|
func TestEmbed_DifferentInputsDifferentOutputs(t *testing.T) {
|
|
net64 := mustCIDR(t, "2602:817:3000:f001::/64")
|
|
a, _ := Embed(net64, []Field{FieldNamespace, FieldPod}, Values{Namespace: "ns1", Pod: "p1"}, 0)
|
|
b, _ := Embed(net64, []Field{FieldNamespace, FieldPod}, Values{Namespace: "ns2", Pod: "p1"}, 0)
|
|
if a.Equal(b) {
|
|
t.Fatalf("different namespace produced identical IID: %s", a)
|
|
}
|
|
}
|
|
|
|
func TestEmbed_NRandomizesLowNibble(t *testing.T) {
|
|
net64 := mustCIDR(t, "2602:817:3000:f001::/64")
|
|
a, _ := Embed(net64, []Field{FieldNamespace}, Values{Namespace: "x"}, 0x1)
|
|
b, _ := Embed(net64, []Field{FieldNamespace}, Values{Namespace: "x"}, 0x2)
|
|
if a.Equal(b) {
|
|
t.Fatalf("changing N did not change address")
|
|
}
|
|
// And the only difference should be the last nibble.
|
|
if a[15]>>4 != b[15]>>4 {
|
|
t.Fatalf("upper nibble of last byte changed unexpectedly: %x vs %x", a[15], b[15])
|
|
}
|
|
}
|
|
|
|
func TestEmbed_RejectsBadInputs(t *testing.T) {
|
|
net64 := mustCIDR(t, "2602:817:3000:f001::/64")
|
|
if _, err := Embed(net64, nil, Values{}, 0); err == nil {
|
|
t.Fatalf("expected error for empty fields")
|
|
}
|
|
odd := &net.IPNet{IP: net.ParseIP("2602:817:3000::"), Mask: net.CIDRMask(63, 128)}
|
|
if _, err := Embed(odd, []Field{FieldNamespace}, Values{Namespace: "x"}, 0); err == nil {
|
|
t.Fatalf("expected error for /63 (not nibble-aligned)")
|
|
}
|
|
v4 := &net.IPNet{IP: net.ParseIP("10.0.0.0").To4(), Mask: net.CIDRMask(8, 32)}
|
|
if _, err := Embed(v4, []Field{FieldNamespace}, Values{Namespace: "x"}, 0); err == nil {
|
|
t.Fatalf("expected error for IPv4 network")
|
|
}
|
|
}
|
|
|
|
func TestEmbed_ImageDigestVsFallback(t *testing.T) {
|
|
net64 := mustCIDR(t, "2602:817:3000:f001::/64")
|
|
digest := "sha256:abcdef0123456789aabbccddeeff00112233445566778899aabbccddeeff0011"
|
|
a, err := Embed(net64, []Field{FieldImage}, Values{Image: digest}, 0)
|
|
if err != nil {
|
|
t.Fatalf("Embed digest: %v", err)
|
|
}
|
|
b, err := Embed(net64, []Field{FieldImage}, Values{ImageFallback: "ctr-xyz"}, 0)
|
|
if err != nil {
|
|
t.Fatalf("Embed fallback: %v", err)
|
|
}
|
|
if a.Equal(b) {
|
|
t.Fatalf("digest and fallback produced same IID")
|
|
}
|
|
}
|