carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net"
8 "net/http"
9 "os"
10 "path/filepath"
11 "slices"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/stopevidence"
19)
20
21// writersKernelFake records what root handed its kernel half.
22type writersKernelFake struct {
23 stopKernelFunc
24 target hostguard.FenceTarget
25 peer StopPeer
26 hints []stopevidence.DescriptorHint
27 err error
28 calls int
29}
30
31func (k *writersKernelFake) VerifyWritersStopped(_ context.Context, t hostguard.FenceTarget, _ StopInventory, peer StopPeer, hints []stopevidence.DescriptorHint) (WritersKernelEvidence, error) {
32 k.calls++
33 k.target, k.peer, k.hints = t, peer, hints
34 if k.err != nil {
35 return WritersKernelEvidence{}, k.err
36 }
37 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
38}
39
40func writersFixtureCoordinator(t *testing.T) (ClaimStopCoordinator, WritersStoppedRequest, *writersKernelFake) {
41 t.Helper()
42 c, req := servingStopFixture(t)
43 kernel := &writersKernelFake{}
44 c.Kernel = kernel
45 w := WritersStoppedRequest{Region: req.Region, Host: req.Host, Incarnation: req.Incarnation, InstallationStamp: req.InstallationStamp, WriterSession: req.WriterSession,
46 Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, ExpectedClaimETag: req.ExpectedClaimETag, ExpectedDirectoryRevision: req.ExpectedDirectoryRevision, RosterRevision: req.RosterRevision,
47 Replicator: []stopevidence.DescriptorHint{{FD: 7, Device: 1, Inode: 2}}}
48 return c, w, kernel
49}
50
51func TestWritersStoppedRootObservesWithItsOwnPeer(t *testing.T) {
52 c, req, kernel := writersFixtureCoordinator(t)
53 ev, err := c.WritersStopped(t.Context(), req)
54 if err != nil {
55 t.Fatal(err)
56 }
57 // The agent's process is the authenticated peer, never a request field.
58 if kernel.peer.PID != 42 || kernel.peer.ProcessStart != "start-42" || !slices.Equal(kernel.hints, req.Replicator) {
59 t.Fatalf("kernel got peer %+v hints %+v", kernel.peer, kernel.hints)
60 }
61 if kernel.target.Session != req.WriterSession || kernel.target.Host != req.Host || kernel.target.FenceID != "" {
62 t.Fatalf("kernel target = %+v", kernel.target)
63 }
64 if ev.Host != req.Host || ev.WriterSession != req.WriterSession || ev.ClaimETag != req.ExpectedClaimETag || ev.CheckedAt.IsZero() || len(ev.Retained) != 1 || ev.AgentPID != 42 {
65 t.Fatalf("evidence = %+v", ev)
66 }
67 // Never stop evidence: nothing is fenced or published.
68 keys, err := c.Evidence.List(t.Context(), "control/")
69 if err != nil || len(keys) != 0 {
70 t.Fatalf("writers-stopped published %v (%v)", keys, err)
71 }
72 called := false
73 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 {
74 t.Fatalf("writers-stopped installed a fence: %v", err)
75 }
76}
77
78func TestWritersStoppedRefuses(t *testing.T) {
79 for name, tc := range map[string]struct {
80 change func(*ClaimStopCoordinator, *WritersStoppedRequest, *writersKernelFake)
81 want error
82 }{
83 "another controller": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) {
84 r.ControllerID = "controller-2"
85 }, ErrClaimStopIdentity},
86 "root's peer": {func(c *ClaimStopCoordinator, _ *WritersStoppedRequest, _ *writersKernelFake) {
87 c.Peers = stopPeerFunc(func(context.Context) (StopPeer, error) {
88 return StopPeer{UID: 0, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"}, nil
89 })
90 }, ErrClaimStopIdentity},
91 "stale claim": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) {
92 r.ExpectedClaimETag = "etag-0"
93 }, ErrClaimStopIdentity},
94 "another session": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.WriterSession = 12 }, ErrClaimStopIdentity},
95 "session zero": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.WriterSession = 0 }, ErrClaimStopUnavailable},
96 "another box": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) { r.Box = "box-2" }, ErrClaimStopIdentity},
97 "another inventory": {func(_ *ClaimStopCoordinator, r *WritersStoppedRequest, _ *writersKernelFake) {
98 r.ExpectedDirectoryRevision = "x"
99 }, ErrClaimStopInventory},
100 "kernel refuses": {func(_ *ClaimStopCoordinator, _ *WritersStoppedRequest, k *writersKernelFake) {
101 k.err = stopevidence.ErrForeignHolder
102 }, stopevidence.ErrForeignHolder},
103 "no kernel half": {func(c *ClaimStopCoordinator, _ *WritersStoppedRequest, _ *writersKernelFake) {
104 c.Kernel = stopKernelFunc(nil)
105 }, ErrClaimStopUnavailable},
106 } {
107 t.Run(name, func(t *testing.T) {
108 c, req, kernel := writersFixtureCoordinator(t)
109 tc.change(&c, &req, kernel)
110 if _, err := c.WritersStopped(t.Context(), req); !errors.Is(err, tc.want) {
111 t.Fatalf("got %v want %v", err, tc.want)
112 }
113 })
114 }
115}
116
117func TestWritersStoppedTransportRoundTrip(t *testing.T) {
118 c, req, _ := writersFixtureCoordinator(t)
119 dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "ws")
120 if err != nil {
121 t.Fatal(err)
122 }
123 defer os.RemoveAll(dir)
124 socket := filepath.Join(dir, "s")
125 l, err := net.Listen("unix", socket)
126 if err != nil {
127 t.Fatal(err)
128 }
129 peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"}
130 h := ClaimStopHTTP{Coordinator: c}
131 srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
132 if r.URL.Path != "/claim/writers-stopped" {
133 w.WriteHeader(http.StatusNotFound)
134 return
135 }
136 h.serveWritersStopped(w, r, peer)
137 }), ReadHeaderTimeout: time.Second}
138 go srv.Serve(l)
139 defer srv.Close()
140 ev, err := CollectWritersStopped(t.Context(), socket, req)
141 if err != nil {
142 t.Fatal(err)
143 }
144 if ev.Host != req.Host || ev.WriterSession != req.WriterSession || len(ev.Retained) != 1 {
145 t.Fatalf("evidence = %+v", ev)
146 }
147 req.Host = "other.example"
148 if _, err := CollectWritersStopped(t.Context(), socket, req); err == nil {
149 t.Fatal("a refused observation passed")
150 }
151}
152
153func TestWritersStoppedRequestIsStrictlyDecoded(t *testing.T) {
154 c, req, _ := writersFixtureCoordinator(t)
155 body, _ := json.Marshal(req)
156 body = append(body[:len(body)-1], []byte(`,"ReplicatorPID":1}`)...)
157 rec := &recordingWriter{header: http.Header{}}
158 r, _ := http.NewRequest(http.MethodPost, "http://control/claim/writers-stopped", strings.NewReader(string(body)))
159 ClaimStopHTTP{Coordinator: c}.serveWritersStopped(rec, r, StopPeer{UID: 1001, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"})
160 if rec.status != http.StatusBadRequest {
161 t.Fatalf("an agent-chosen pid field was accepted: %d", rec.status)
162 }
163}
164
165type recordingWriter struct {
166 header http.Header
167 status int
168 body []byte
169}
170
171func (w *recordingWriter) Header() http.Header { return w.header }
172func (w *recordingWriter) Write(b []byte) (int, error) {
173 if w.status == 0 {
174 w.status = http.StatusOK
175 }
176 w.body = append(w.body, b...)
177 return len(b), nil
178}
179func (w *recordingWriter) WriteHeader(s int) { w.status = s }
180