flock M1 scaffold: CNI plugin + agent + NodeConfig CRD
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>
This commit is contained in:
Donavan Fritz
2026-04-24 21:17:42 -05:00
commit 20f47916af
22 changed files with 1460 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
package agent
import (
"os"
"path/filepath"
"testing"
"time"
)
func newStore(t *testing.T) (*Store, string) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "allocations.json")
s, err := NewStore(path, "host001")
if err != nil {
t.Fatalf("NewStore: %v", err)
}
return s, path
}
func TestStore_EmptyOnFirstOpen(t *testing.T) {
s, _ := newStore(t)
if got := len(s.Snapshot()); got != 0 {
t.Fatalf("Snapshot len = %d, want 0", got)
}
}
func TestStore_UpsertGetDelete(t *testing.T) {
s, path := newStore(t)
a := Allocation{
ContainerID: "abc",
Namespace: "mail",
PodName: "stalwart-0",
OwnerUID: "uid-1",
IP6: "2602:817:3000:f001::1",
State: StateCommitted,
AllocatedAt: time.Now().UTC().Truncate(time.Second),
}
if err := s.Upsert(a); err != nil {
t.Fatalf("Upsert: %v", err)
}
got, ok := s.Get("abc")
if !ok || got.PodName != "stalwart-0" {
t.Fatalf("Get after Upsert: ok=%v got=%+v", ok, got)
}
// Round-trip: a fresh Store reading the same path sees the entry.
s2, err := NewStore(path, "host001")
if err != nil {
t.Fatalf("reopen: %v", err)
}
if got, ok := s2.Get("abc"); !ok || got.IP6 != a.IP6 {
t.Fatalf("reopen Get: ok=%v got=%+v", ok, got)
}
if err := s.Delete("abc"); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, ok := s.Get("abc"); ok {
t.Fatalf("entry still present after Delete")
}
}
func TestStore_UpsertReplacesByContainerID(t *testing.T) {
s, _ := newStore(t)
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(s.Upsert(Allocation{ContainerID: "abc", IP6: "::1", State: StatePending}))
must(s.Upsert(Allocation{ContainerID: "abc", IP6: "::2", State: StateCommitted}))
if got := len(s.Snapshot()); got != 1 {
t.Fatalf("len = %d, want 1 (Upsert should replace)", got)
}
if a, _ := s.Get("abc"); a.IP6 != "::2" || a.State != StateCommitted {
t.Fatalf("Upsert did not replace: %+v", a)
}
}
func TestStore_PendingContainerIDs(t *testing.T) {
s, _ := newStore(t)
_ = s.Upsert(Allocation{ContainerID: "p1", State: StatePending})
_ = s.Upsert(Allocation{ContainerID: "c1", State: StateCommitted})
_ = s.Upsert(Allocation{ContainerID: "p2", State: StatePending})
pend := s.PendingContainerIDs()
if len(pend) != 2 {
t.Fatalf("PendingContainerIDs len = %d, want 2", len(pend))
}
have := map[string]bool{pend[0]: true, pend[1]: true}
if !have["p1"] || !have["p2"] {
t.Fatalf("PendingContainerIDs = %v, want p1,p2", pend)
}
}
func TestStore_RejectsWrongVersion(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "allocations.json")
if err := os.WriteFile(path, []byte(`{"version":99,"node":"x","allocations":[]}`), 0o600); err != nil {
t.Fatal(err)
}
if _, err := NewStore(path, "x"); err == nil {
t.Fatalf("expected error on bad version, got nil")
}
}
func TestStore_AtomicWriteDurability(t *testing.T) {
// We can't simulate a real power-loss in unit tests, but we can verify
// that no .tmp file is left behind after a successful flush, and that
// the rename target is intact.
s, path := newStore(t)
if err := s.Upsert(Allocation{ContainerID: "x", State: StateCommitted}); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Fatalf(".tmp leaked: err=%v", err)
}
b, err := os.ReadFile(path)
if err != nil || len(b) == 0 {
t.Fatalf("final file unreadable: err=%v len=%d", err, len(b))
}
}