M2: netlink, IPAM/handler wiring, BIRD sidecar, CNI installer
Build flock Image / build (push) Has been cancelled

Code (Linux build, with no-op stubs for macOS dev):
- pkg/agent/netns_linux.go: ensureVeth → host-side configure (addrgenmode
  none, fe80::1/64, proxy_arp, forwarding) → move peer to pod ns →
  configure pod side (addr, default route via fe80::1, v4 169.254.1.1
  on-link gateway) → host /128 + /32 routes. Idempotent.
- pkg/agent/hostiface.go: deterministic host iface name flock<8hex> from
  FNV-1a-32(containerID).
- pkg/agent/annotations.go: parse flock.fritzlab.net/{ipv6,ipv4,cidr6,
  cidr4,ip-algo,anycast} with design-doc defaults; ParseCNIArgs for the
  K8S_POD_* keys kubelet sets.
- pkg/agent/podinfo.go: shared informer scoped to spec.nodeName==NODE,
  WaitForPod helper for ADD-vs-informer-sync race.
- pkg/agent/handlers.go: PodHandler does
    cache lookup → annotations → IPAM → store(pending) → SetupFunc →
    store(committed) → Result. Idempotent on retry. Del symmetric.
- pkg/routing/bird/config.go: text/template render with stable ordering;
  golden tests for host001 + anycast injection + sort stability.
- pkg/agent/bird.go: writes /etc/flock/bird/bird.conf, debounces 500ms,
  execs `birdc -s /run/flock/bird.ctl configure`. Installs blackhole
  kernel routes for the node summary CIDRs so BIRD's protocol kernel
  imports them.
- pkg/agent/runtime_linux.go: at startup, waits up to 60s for the per-
  node NodeConfig, reconciles committed allocations into IPAM.used,
  garbage-collects pending entries, builds PodHandler, swaps RPC
  handlers in.
- cmd/flock-installer: init-container binary that copies /opt/cni/bin/
  flock and writes 01-flock.conflist (lex-first so kubelet picks it
  over Calico's 10-calico.conflist on flock-labeled nodes).

Deploy:
- Dockerfile: alpine + iproute2 + bird2; multi-binary image.
- deploy/daemonset.yaml: install-cni init container; bird sidecar
  sharing /etc/flock/bird + /run/flock with the agent; ConfigMap-seeded
  bootstrap bird.conf so the sidecar boots before the agent renders.
  Privileged on flock-agent + install-cni; bird sidecar uses
  NET_ADMIN/RAW only.
- RBAC: pods + networkpolicies get/list/watch (the latter is reserved
  for M8 — harmless to grant now).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-24 22:33:48 -05:00
parent 31fcae2a97
commit eb1f5e0d8d
20 changed files with 1688 additions and 61 deletions
+74
View File
@@ -0,0 +1,74 @@
package agent
import (
"testing"
"code.fritzlab.net/fritzlab/flock/pkg/embed"
)
func TestParseAnnotations_Defaults(t *testing.T) {
a, err := ParseAnnotations(nil)
if err != nil {
t.Fatal(err)
}
if !a.WantV6 || a.WantV4 {
t.Fatalf("defaults wrong: v6=%v v4=%v", a.WantV6, a.WantV4)
}
}
func TestParseAnnotations_DualStack(t *testing.T) {
a, err := ParseAnnotations(map[string]string{
annotationPrefix + "ipv4": "true",
})
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) {
if _, err := ParseAnnotations(map[string]string{
annotationPrefix + "ipv6": "false",
}); err == nil {
t.Fatalf("expected error: ipv6=false ipv4=false")
}
}
func TestParseAnnotations_IPAlgo(t *testing.T) {
a, err := ParseAnnotations(map[string]string{
annotationPrefix + "ip-algo": "namespace,pod,image",
})
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_CIDR(t *testing.T) {
a, err := ParseAnnotations(map[string]string{
annotationPrefix + "cidr6": "2602:817:3000:f001::/64, 2602:817:3000:f002::/64",
})
if err != nil {
t.Fatal(err)
}
if len(a.CIDR6) != 2 {
t.Fatalf("cidr6 len=%d", len(a.CIDR6))
}
}
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)
}
}