package host import ( "context" "encoding/json" "errors" "net" "net/http" "os" "path/filepath" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/stopevidence" ) // writersKernelFake records what root handed its kernel half. type writersKernelFake struct { stopKernelFunc target hostguard.FenceTarget peer StopPeer hints []stopevidence.DescriptorHint err error calls int } func (k *writersKernelFake) VerifyWritersStopped(_ context.Context, t hostguard.FenceTarget, _ StopInventory, peer StopPeer, hints []stopevidence.DescriptorHint) (WritersKernelEvidence, error) { k.calls++ k.target, k.peer, k.hints = t, peer, hints if k.err != nil { return WritersKernelEvidence{}, k.err } return WritersKernelEvidence{CheckedPaths: []string{"/data/shop/data.db"}, CgroupIdentities: []string{"cg:1:2"}, Retained: []stopevidence.Descriptor{{PID: peer.PID, TID: peer.PID, FD: 7, Device: 1, Inode: 2}}}, nil } func writersFixtureCoordinator(t *testing.T) (ClaimStopCoordinator, WritersStoppedRequest, *writersKernelFake) { t.Helper() c, req := servingStopFixture(t) kernel := &writersKernelFake{} c.Kernel = kernel w := WritersStoppedRequest{Region: req.Region, Host: req.Host, Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, WriterSession: req.WriterSession, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ExpectedClaimETag: req.ExpectedClaimETag, ExpectedDirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision, Replicator: []stopevidence.DescriptorHint{{FD: 7, Device: 1, Inode: 2}}} return c, w, kernel } func TestWritersStoppedRootObservesWithItsOwnPeer(t *testing.T) { c, req, kernel := writersFixtureCoordinator(t) ev, err := c.WritersStopped(t.Context(), req) if err != nil { t.Fatal(err) } // The agent's process is the authenticated peer, never a request field. if kernel.peer.PID != 42 || kernel.peer.ProcessStart != "start-42" || !slices.Equal(kernel.hints, req.Replicator) { t.Fatalf("kernel got peer %+v hints %+v", kernel.peer, kernel.hints) } if kernel.target.Session != req.WriterSession || kernel.target.Host != req.Host || kernel.target.FenceID != "" { t.Fatalf("kernel target = %+v", kernel.target) } if ev.Host != req.Host || ev.WriterSession != req.WriterSession || ev.ClaimETag != req.ExpectedClaimETag || ev.CheckedAt.IsZero() || len(ev.Retained) != 1 || ev.AgentPID != 42 { t.Fatalf("evidence = %+v", ev) } // Never stop evidence: nothing is fenced or published. keys, err := c.Evidence.List(t.Context(), "control/") if err != nil || len(keys) != 0 { t.Fatalf("writers-stopped published %v (%v)", keys, err) } called := false if err := c.Guard.With(t.Context(), req.Host, hostguard.Captured{Incarnation: req.Incarnation, Stamp: req.InstallationStamp}, func(*hostclaim.Claim) error { return nil }, func() error { called = true; return nil }); err != nil || !called { t.Fatalf("writers-stopped installed a fence: %v", err) } } func TestWritersStoppedRefuses(t *testing.T) { for name, tc := range map[string]struct { change func(*ClaimStopCoordinator, *WritersStoppedRequest, *writersKernelFake) want error }{ "another controller": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.ControllerID = "controller-2" }, ErrClaimStopIdentity}, "root's peer": {func(c *ClaimStopCoordinator, _ *WritersStoppedRequest, _ *writersKernelFake) { c.Peers = stopPeerFunc(func(context.Context) (StopPeer, error) { return StopPeer{UID: 0, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"}, nil }) }, ErrClaimStopIdentity}, "stale claim": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.ExpectedClaimETag = "etag-0" }, ErrClaimStopIdentity}, "another session": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.WriterSession = 12 }, ErrClaimStopIdentity}, "session zero": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.WriterSession = 0 }, ErrClaimStopUnavailable}, "another box": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.Box = "box-2" }, ErrClaimStopIdentity}, "another inventory": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.ExpectedDirectoryRevision = "x" }, ErrClaimStopInventory}, "kernel refuses": {func(_ *ClaimStopCoordinator, _ *WritersStoppedRequest, k *writersKernelFake) { k.err = stopevidence.ErrForeignHolder }, stopevidence.ErrForeignHolder}, "no kernel half": {func(c *ClaimStopCoordinator, _ *WritersStoppedRequest, _ *writersKernelFake) { c.Kernel = stopKernelFunc(nil) }, ErrClaimStopUnavailable}, } { t.Run(name, func(t *testing.T) { c, req, kernel := writersFixtureCoordinator(t) tc.change(&c, &req, kernel) if _, err := c.WritersStopped(t.Context(), req); !errors.Is(err, tc.want) { t.Fatalf("got %v want %v", err, tc.want) } }) } } func TestWritersStoppedTransportRoundTrip(t *testing.T) { c, req, _ := writersFixtureCoordinator(t) dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "ws") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) socket := filepath.Join(dir, "s") l, err := net.Listen("unix", socket) if err != nil { t.Fatal(err) } peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"} h := ClaimStopHTTP{Coordinator: c} srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/claim/writers-stopped" { w.WriteHeader(http.StatusNotFound) return } h.serveWritersStopped(w, r, peer) }), ReadHeaderTimeout: time.Second} go srv.Serve(l) defer srv.Close() ev, err := CollectWritersStopped(t.Context(), socket, req) if err != nil { t.Fatal(err) } if ev.Host != req.Host || ev.WriterSession != req.WriterSession || len(ev.Retained) != 1 { t.Fatalf("evidence = %+v", ev) } req.Host = "other.example" if _, err := CollectWritersStopped(t.Context(), socket, req); err == nil { t.Fatal("a refused observation passed") } } func TestWritersStoppedRequestIsStrictlyDecoded(t *testing.T) { c, req, _ := writersFixtureCoordinator(t) body, _ := json.Marshal(req) body = append(body[:len(body)-1], []byte(`,"ReplicatorPID":1}`)...) rec := &recordingWriter{header: http.Header{}} r, _ := http.NewRequest(http.MethodPost, "http://control/claim/writers-stopped", strings.NewReader(string(body))) ClaimStopHTTP{Coordinator: c}.serveWritersStopped(rec, r, StopPeer{UID: 1001, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"}) if rec.status != http.StatusBadRequest { t.Fatalf("an agent-chosen pid field was accepted: %d", rec.status) } } type recordingWriter struct { header http.Header status int body []byte } func (w *recordingWriter) Header() http.Header { return w.header } func (w *recordingWriter) Write(b []byte) (int, error) { if w.status == 0 { w.status = http.StatusOK } w.body = append(w.body, b...) return len(b), nil } func (w *recordingWriter) WriteHeader(s int) { w.status = s }