carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "errors"
8 "net"
9 "net/http"
10 "os"
11 "path/filepath"
12 "reflect"
13 "sync/atomic"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/instancectl"
20 "github.com/carlosframework/platform/internal/rootstate"
21)
22
23func servingParkSocket(t *testing.T, handler http.Handler) hostbox.JournalCall {
24 t.Helper()
25 return rootJournalClient(servingRootSocket(t, handler))
26}
27func servingRootSocket(t *testing.T, handler http.Handler) string {
28 t.Helper()
29 dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "serving-park-")
30 if err != nil {
31 t.Fatal(err)
32 }
33 t.Cleanup(func() { os.RemoveAll(dir) })
34 path := filepath.Join(dir, "root.sock")
35 listener, err := net.Listen("unix", path)
36 if err != nil {
37 t.Fatal(err)
38 }
39 server := &instancectl.Server{ClaimStop: handler, PeerOK: func(net.Conn) error { return nil }, Logf: func(string, ...any) {}}
40 ctx, cancel := context.WithCancel(context.Background())
41 done := make(chan error, 1)
42 go func() { done <- server.Serve(ctx, listener) }()
43 t.Cleanup(func() {
44 cancel()
45 if err := <-done; err != nil {
46 t.Error(err)
47 }
48 })
49 return path
50}
51
52func servingTransportRoot(t *testing.T, f *servingRootFixture) ClaimRootHTTP {
53 t.Helper()
54 ctx, cancel := context.WithCancel(context.Background())
55 p := &servingParkRuntime{authority: f.p, state: servingBorrowedState{f.p.ledger.state}, ctx: ctx, cancel: cancel, idle: make(chan struct{})}
56 t.Cleanup(func() {
57 cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second)
58 defer cancel()
59 if err := p.Close(cleanup); err != nil {
60 t.Error(err)
61 }
62 })
63 h := f.p.root
64 h.servingParks = p
65 h.Stop.Coordinator.RetainNoProcess = p.retainNoProcess
66 return h
67}
68
69func TestServingParkSocketRetiresAndCompletes(t *testing.T) {
70 for _, legacy := range []bool{false, true} {
71 t.Run(map[bool]string{false: "generation", true: "legacy"}[legacy], func(t *testing.T) {
72 var f *servingRootFixture
73 if legacy {
74 f, _ = newServingLegacyRootFixture(t, false)
75 } else {
76 f = newServingRootFixture(t, true)
77 }
78 h := servingTransportRoot(t, f)
79 call := servingParkSocket(t, h)
80 var exact hostbox.JournalRequest
81 client := hostbox.ServingParkClient{Call: func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) {
82 exact = q
83 return call(ctx, q)
84 }}
85 var a servingParkAuthorization
86 var err error
87 if legacy {
88 a, err = client.ParkLegacy(t.Context(), f.current, servingLegacyPredecessor{})
89 } else {
90 a, err = client.Park(t.Context(), f.current, f.prior)
91 }
92 if err != nil {
93 t.Fatal(err)
94 }
95 parkRequest := exact
96 r, err := f.p.ledger.read(a.ID)
97 if err != nil || !r.Parked || r.Complete {
98 t.Fatalf("park: %+v %v", r, err)
99 }
100 if _, err = os.Stat(f.db); !errors.Is(err, os.ErrNotExist) {
101 t.Fatal("source survived successful park", err)
102 }
103 f.tags(f.current.Stamp)
104 if err = client.Complete(t.Context(), a.ID, f.current); err != nil {
105 t.Fatal(err)
106 }
107 r, err = f.p.ledger.read(a.ID)
108 if err != nil || !r.Complete {
109 t.Fatalf("completion: %+v %v", r, err)
110 }
111 if !legacy {
112 again, err := client.Park(t.Context(), f.current, f.prior)
113 if err != nil || again != a {
114 t.Fatalf("fresh request after completion: %+v %v", again, err)
115 }
116 if exact.ID == parkRequest.ID {
117 t.Fatal("fresh request reused cached transport reply")
118 }
119 }
120 before, err := f.disk.Read("retirement-" + a.ID + ".json")
121 if err != nil {
122 t.Fatal(err)
123 }
124 // A replay returns the immutable reply even after restore writes the
125 // successor. It must not inspect or park those new bytes.
126 if err = os.WriteFile(f.db, []byte("successor"), 0600); err != nil {
127 t.Fatal(err)
128 }
129 out, err := call(t.Context(), parkRequest)
130 if err != nil || out.Error != "" || out.ServingPark == nil || *out.ServingPark != a {
131 t.Fatalf("replay: %+v %v", out, err)
132 }
133 after, err := f.disk.Read("retirement-" + a.ID + ".json")
134 if err != nil || string(before) != string(after) {
135 t.Fatal("replay changed retirement")
136 }
137 body, _ := os.ReadFile(f.db)
138 if string(body) != "successor" {
139 t.Fatal("replay touched successor")
140 }
141 })
142 }
143}
144
145func TestServingParkSocketLostReplyReplaysExactRequest(t *testing.T) {
146 f := newServingRootFixture(t, true)
147 h := servingTransportRoot(t, f)
148 var requests atomic.Int32
149 call := servingParkSocket(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150 if requests.Add(1) == 1 {
151 // Execute against the real connection context, then drop the reply.
152 h.ServeHTTP(discardServingReply{HeaderMap: make(http.Header)}, r)
153 conn, _, err := w.(http.Hijacker).Hijack()
154 if err == nil {
155 conn.Close()
156 }
157 return
158 }
159 h.ServeHTTP(w, r)
160 }))
161 client := hostbox.ServingParkClient{Call: call}
162 a, err := client.Park(t.Context(), f.current, f.prior)
163 if err != nil || requests.Load() != 2 {
164 t.Fatalf("lost reply: %+v %v requests=%d", a, err, requests.Load())
165 }
166 r, err := f.p.ledger.read(a.ID)
167 if err != nil || !r.Parked {
168 t.Fatalf("park: %+v %v", r, err)
169 }
170}
171
172type discardServingReply struct{ HeaderMap http.Header }
173
174func (w discardServingReply) Header() http.Header { return w.HeaderMap }
175func (discardServingReply) WriteHeader(int) {}
176func (discardServingReply) Write(b []byte) (int, error) { return len(b), nil }
177
178func TestServingParkSocketAuthorityGates(t *testing.T) {
179 for _, gate := range []string{"peer", "census", "enrollment", "revision", "box", "quarantine"} {
180 t.Run(gate, func(t *testing.T) {
181 f := newServingRootFixture(t, true)
182 h := servingTransportRoot(t, f)
183 var refuse atomic.Bool
184 auth := h.Stop.Authenticate
185 h.Stop.Authenticate = func(ctx context.Context, c net.Conn) (StopPeer, error) {
186 if refuse.Load() && gate == "peer" {
187 return StopPeer{}, ErrClaimStopIdentity
188 }
189 return auth(ctx, c)
190 }
191 h.RequireNewWork = func(context.Context) error {
192 if refuse.Load() && gate == "census" {
193 return hostbox.ErrChanged
194 }
195 return nil
196 }
197 h.InventoryActor = func(context.Context) (hostbox.Actor, error) {
198 a := f.actor
199 if refuse.Load() {
200 switch gate {
201 case "enrollment":
202 a.Enrollment = "other"
203 case "revision":
204 a.Revision = 0
205 case "box":
206 a.Box = "other"
207 case "quarantine":
208 a.Quarantined = true
209 }
210 }
211 return a, nil
212 }
213 call := servingParkSocket(t, h)
214 var q hostbox.JournalRequest
215 client := hostbox.ServingParkClient{Call: func(ctx context.Context, in hostbox.JournalRequest) (hostbox.JournalResponse, error) {
216 q = in
217 return call(ctx, in)
218 }}
219 a, err := client.Park(t.Context(), f.current, f.prior)
220 if err != nil {
221 t.Fatal(err)
222 }
223 before, _ := f.disk.Read("retirement-" + a.ID + ".json")
224 refuse.Store(true)
225 out, err := call(t.Context(), q)
226 if err == nil && out.Error == "" {
227 t.Fatal("replay bypassed authority", gate)
228 }
229 after, _ := f.disk.Read("retirement-" + a.ID + ".json")
230 if !reflect.DeepEqual(before, after) {
231 t.Fatal("refusal mutated record")
232 }
233 })
234 }
235}
236
237type servingCloseStore struct {
238 rootstate.Store
239 closed atomic.Bool
240}
241
242func (s *servingCloseStore) Close() error { s.closed.Store(true); return nil }
243
244func TestServingParkRuntimeJoinBeforePrivateStateClose(t *testing.T) {
245 s := &servingCloseStore{}
246 ctx, cancel := context.WithCancel(context.Background())
247 p := &servingParkRuntime{state: s, ctx: ctx, cancel: cancel, idle: make(chan struct{})}
248 call, done, err := p.begin(t.Context())
249 if err != nil {
250 t.Fatal(err)
251 }
252 deadline, stop := context.WithCancel(context.Background())
253 stop()
254 if err = p.Close(deadline); !errors.Is(err, context.Canceled) || s.closed.Load() {
255 t.Fatal("closed state before join", err)
256 }
257 select {
258 case <-call.Done():
259 case <-time.After(time.Second):
260 t.Fatal("active call not canceled")
261 }
262 if _, _, err = p.begin(t.Context()); err == nil {
263 t.Fatal("new call admitted during close")
264 }
265 done()
266 if err = p.Close(t.Context()); err != nil || !s.closed.Load() {
267 t.Fatal("join did not close store", err)
268 }
269 if err = p.Close(t.Context()); err != nil {
270 t.Fatal(err)
271 }
272}
273
274func TestServingParkRuntimeRetainsNoProcess(t *testing.T) {
275 // The production lifecycle wrapper must retain the same root observation
276 // as the direct coordinator fixture, and refuse after shutdown.
277 f, _ := newServingLegacyRootFixture(t, false)
278 h := servingTransportRoot(t, f)
279 saved, err := f.p.ledger.legacyStop(f.current)
280 if err != nil {
281 t.Fatal(err)
282 }
283 // Retention rechecks the original claim, so it refuses after admission.
284 if err = h.Stop.Coordinator.RetainNoProcess(t.Context(), saved.Observed, saved.Result); err == nil {
285 t.Fatal("retained stale preadmission observation")
286 }
287 if err = h.servingParks.Close(t.Context()); err != nil {
288 t.Fatal(err)
289 }
290 if err = h.Stop.Coordinator.RetainNoProcess(t.Context(), hostclaim.Observed{}, StopResult{}); err == nil {
291 t.Fatal("closed runtime retained evidence")
292 }
293}
294
295func TestServingParkProductionConstructorRequiresPrivateDependencies(t *testing.T) {
296 f := newServingRootFixture(t, true)
297 if _, err := newServingParkRuntime(f.p.root, nil, nil); err == nil {
298 t.Fatal("missing private dependencies accepted")
299 }
300 // Production FS construction fixes root ownership and the nosymfollow
301 // check. This test does not claim to open a privileged rootstate store.
302 p, err := newServingParkRuntime(f.p.root, f.disk, plan9D5Fixture(t).owners)
303 if err != nil {
304 t.Fatal(err)
305 }
306 defer p.Close(t.Context())
307 fs := p.authority.(*servingParkRoot).ledger.fs.(*servingParkFS)
308 if fs.owner != 0 || fs.mount == nil || p.authority.(*servingParkRoot).ledger.journal != f.p.root.Journals.Launches {
309 t.Fatal("production constructor replaced root authority")
310 }
311 ctx, cancel := context.WithTimeout(t.Context(), time.Second)
312 defer cancel()
313 if err = p.Close(ctx); err != nil {
314 t.Fatal(err)
315 }
316}
317
318func TestServingParkProductionBindingRetainsSessionZero(t *testing.T) {
319 old := openClaimServingParkAuthority
320 t.Cleanup(func() { openClaimServingParkAuthority = old })
321 var closed *servingCloseStore
322 f, _ := newServingLegacyRootFixture(t, false, func(f *servingRootFixture, coordinator *ClaimStopCoordinator, request StopRequest) {
323 closed = &servingCloseStore{Store: f.disk}
324 openClaimServingParkAuthority = func() (rootstate.Store, error) { return closed, nil }
325 h, err := withServingParkRoot(f.p.root, f.p.empty)
326 if err != nil {
327 t.Fatal(err)
328 }
329 t.Cleanup(func() {
330 if err := h.servingParks.Close(context.Background()); err != nil {
331 t.Error(err)
332 }
333 })
334 // This repeats the real preadmission coordinator path through production
335 // binding. The original private no-process record must remain identical.
336 key := servingLegacyKey("legacy-stop", servingArtifactIdentity{Host: request.Host, Box: request.Box, Incarnation: request.Incarnation})
337 before, err := f.disk.Read(key)
338 if err != nil {
339 t.Fatal(err)
340 }
341 coordinator.RetainNoProcess = h.Stop.Coordinator.RetainNoProcess
342 if _, err = coordinator.FenceAndStop(t.Context(), request); err != nil {
343 t.Fatal(err)
344 }
345 after, err := f.disk.Read(key)
346 if err != nil || !reflect.DeepEqual(before, after) {
347 t.Fatal("production retention changed original evidence", err)
348 }
349 if closed.closed.Load() {
350 t.Fatal("private store closed during service")
351 }
352 if err = h.servingParks.Close(t.Context()); err != nil {
353 t.Fatal(err)
354 }
355 })
356 if f == nil || closed == nil || !closed.closed.Load() {
357 t.Fatal("production private store leaked")
358 }
359 bad := &servingCloseStore{Store: f.disk}
360 openClaimServingParkAuthority = func() (rootstate.Store, error) { return bad, nil }
361 if _, err := withServingParkRoot(ClaimRootHTTP{}, nil); err == nil || !bad.closed.Load() {
362 t.Fatal("failed binding leaked private store", err)
363 }
364}
365
366type servingBorrowedState struct{ rootstate.Store }
367
368func (servingBorrowedState) Close() error { return nil }
369