package netpol import ( "context" "io" "log/slog" "net" "strings" "sync" "sync/atomic" "testing" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // fakeApplier captures Apply calls for assertion. Drop-in for *Applier in // tests because Reconciler depends only on the (Apply, Clear) pair. type fakeApplier struct { mu sync.Mutex calls []string last string err error } func (f *fakeApplier) Apply(_ context.Context, script string) error { f.mu.Lock() defer f.mu.Unlock() if f.err != nil { return f.err } if script == f.last { return nil // de-dup like the real Applier } f.last = script f.calls = append(f.calls, script) return nil } func (f *fakeApplier) Clear(_ context.Context) error { return nil } func (f *fakeApplier) lastScript() string { f.mu.Lock() defer f.mu.Unlock() return f.last } func (f *fakeApplier) callCount() int { f.mu.Lock() defer f.mu.Unlock() return len(f.calls) } // applierIface is satisfied by *Applier and *fakeApplier; we narrow // Reconciler to this in tests by adapting via a tiny wrapper. type applierIface interface { Apply(context.Context, string) error Clear(context.Context) error } // reconcileOnce drives one pass synchronously without spinning a goroutine. func reconcileOnce(t *testing.T, world *World, local LocalPodSource, app applierIface) { t.Helper() in := Inputs{ LocalPods: local(), PeerPods: world.snapshotPeerPods(), Namespaces: world.snapshotNamespaces(), Policies: world.snapshotPolicies(), } out, err := Translate(in, nil) if err != nil { t.Fatal(err) } if err := app.Apply(context.Background(), Render(out)); err != nil { t.Fatal(err) } } // silentLogger returns a slog.Logger discarding everything — keeps test // output tidy. func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})) } func TestReconciler_NoIsolatedPods_ShortScript(t *testing.T) { world := NewWorld(silentLogger()) local := func() []Pod { return nil } app := &fakeApplier{} reconcileOnce(t, world, local, app) got := app.lastScript() if !strings.Contains(got, "table inet flock_netpol") { t.Fatalf("missing table:\n%s", got) } // Without any isolated pods the base chain has policy accept and no // jumps. That's the desired "open" state. if strings.Contains(got, "jump pod_") { t.Fatalf("unexpected jump in open state:\n%s", got) } } func TestReconciler_PolicyIsolatesLocalPod(t *testing.T) { world := NewWorld(silentLogger()) // Seed a default-deny policy in ns1. world.onPolicy(&netv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "deny-all"}, Spec: netv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{}, PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress}, }, }, false) local := func() []Pod { return []Pod{{ Namespace: "ns1", Name: "web", Labels: map[string]string{"app": "web"}, HostIface: "flock00000001", IPs: []net.IP{mustIP("2001:db8::1")}, }} } app := &fakeApplier{} reconcileOnce(t, world, local, app) got := app.lastScript() if !strings.Contains(got, "_ingress {") { t.Fatalf("expected pod ingress chain:\n%s", got) } if !strings.Contains(got, "drop") { t.Fatalf("expected default-deny drop:\n%s", got) } if !strings.Contains(got, `oifname "flock00000001"`) { t.Fatalf("expected base-chain jump anchored on veth:\n%s", got) } } func TestReconciler_DedupesIdenticalRender(t *testing.T) { world := NewWorld(silentLogger()) local := func() []Pod { return []Pod{{ Namespace: "ns1", Name: "web", HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")}, }} } app := &fakeApplier{} reconcileOnce(t, world, local, app) reconcileOnce(t, world, local, app) reconcileOnce(t, world, local, app) if got := app.callCount(); got != 1 { t.Fatalf("expected 1 unique apply, got %d", got) } } func TestReconciler_OnChangeFiresTrigger(t *testing.T) { world := NewWorld(silentLogger()) var triggered atomic.Int32 world.OnChange(func() { triggered.Add(1) }) world.onNamespace(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, false) world.onPolicy(&netv1.NetworkPolicy{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "p"}}, false) if triggered.Load() != 2 { t.Fatalf("expected 2 OnChange calls, got %d", triggered.Load()) } }