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/base64"
6 "errors"
7 "fmt"
8 "github.com/carlosframework/platform/internal/activator"
9 "github.com/carlosframework/platform/internal/genselect"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "reflect"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/hostwrite"
22 "github.com/carlosframework/platform/internal/moves"
23 "github.com/carlosframework/platform/internal/provision"
24 "github.com/carlosframework/platform/internal/registry"
25)
26
27func TestPlan10G2ReadAccess(t *testing.T) {
28 t.Parallel()
29 f := newAdminTokenFixture(t, "box-1")
30 ctx := t.Context()
31 o, err := hostclaim.Read(ctx, f.claims, adminTokenHost)
32 if err != nil {
33 t.Fatal(err)
34 }
35 o, err = hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error {
36 c.Op = hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: hostclaim.PhaseProbing, Exec: "box-1", Participants: []string{"box-0", "box-1"}, Intent: map[string]string{"source": "box-0", "target": "box-1", "to": "ie"}}
37 return nil
38 })
39 if err != nil {
40 t.Fatal(err)
41 }
42 if err = f.tokens.Guard.Activate(ctx, adminTokenHost, "box-1", nil); err != nil {
43 t.Fatal(err)
44 }
45 captured, err := hostguard.Capture(f.data, adminTokenHost)
46 if err != nil {
47 t.Fatal(err)
48 }
49 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer reg.Close()
54 if err = reg.Upsert(f.rt); err != nil {
55 t.Fatal(err)
56 }
57 reads, launches, prepared := 0, 0, 0
58 original := ClaimServingLaunch{Args: []string{"arg"}, Env: []string{"A=B"}}
59 report := moves.EnvReport{Vars: map[string]string{"A": "hash"}}
60 a := &claimMoveReadAccess{actor: f.actor, check: claimBoundIdentity(f.verifyActor, f.actor), route: func(host string) (*registry.Route, error) { reads++; return reg.Get(host) }, launch: func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
61 launches++
62 return original, report, nil
63 }}
64 checkAll := func(want error) {
65 t.Helper()
66 if _, err := a.Route(ctx, adminTokenHost); !errors.Is(err, want) {
67 t.Fatalf("route: %v", err)
68 }
69 if _, _, err := a.Launch(ctx, adminTokenHost); !errors.Is(err, want) {
70 t.Fatalf("launch: %v", err)
71 }
72 if _, err := a.HeldWork(ctx, adminTokenHost, "held-config", captured); !errors.Is(err, want) {
73 t.Fatalf("work: %v", err)
74 }
75 }
76 checkAll(hostwrite.ErrUnavailable)
77 if reads != 0 || launches != 0 {
78 t.Fatal("access before Build")
79 }
80 source := hostbox.WorkSource{Guard: f.tokens.Guard, Box: f.actor.Box, Region: f.actor.Region, Now: time.Now, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }, Prepare: func(context.Context, hostclaim.Observed) error { prepared++; return nil }}
81 f.verify = hostbox.ErrChanged
82 if err = a.bind(ctx, source); !errors.Is(err, hostbox.ErrChanged) {
83 t.Fatal("binding without verification", err)
84 }
85 checkAll(hostwrite.ErrUnavailable)
86 f.verify = nil
87 if err = a.bind(ctx, source); err != nil {
88 t.Fatal(err)
89 }
90 if _, err = a.HeldWork(ctx, adminTokenHost, "held-config", captured); err != nil {
91 t.Fatal(err)
92 }
93 if prepared != 0 {
94 t.Fatal("read-only selection called Prepare")
95 }
96 for _, action := range []string{"spawn", "install", "held-timers", "delete"} {
97 if _, err = a.HeldWork(ctx, adminTokenHost, action, captured); !errors.Is(err, hostbox.ErrUnsupported) {
98 t.Fatalf("action %s: %v", action, err)
99 }
100 }
101 rt, err := a.Route(ctx, adminTokenHost)
102 if err != nil {
103 t.Fatal(err)
104 }
105 rt.DBPath = "foreign"
106 stored, err := reg.Get(adminTokenHost)
107 if err != nil || stored.DBPath != f.rt.DBPath {
108 t.Fatal("route read changed registry", err)
109 }
110 launch, got, err := a.Launch(ctx, adminTokenHost)
111 if err != nil {
112 t.Fatal(err)
113 }
114 launch.Env[0] = "changed"
115 got.Vars["A"] = "changed"
116 if original.Env[0] != "A=B" || report.Vars["A"] != "hash" {
117 t.Fatal("accessor aliases owner data")
118 }
119 // A present host executed by another box is foreign too.
120 current, err := hostclaim.Read(ctx, f.claims, adminTokenHost)
121 if err != nil {
122 t.Fatal(err)
123 }
124 current, err = hostclaim.Update(ctx, f.claims, current, func(c *hostclaim.Claim) error { c.Op.Exec = "box-2"; return nil })
125 if err != nil {
126 t.Fatal(err)
127 }
128 checkAll(hostbox.ErrChanged)
129 if _, err = hostclaim.Update(ctx, f.claims, current, func(c *hostclaim.Claim) error { c.Op.Exec = "box-1"; return nil }); err != nil {
130 t.Fatal(err)
131 }
132 previous := reads
133 if _, err = a.Route(ctx, "foreign.example"); !errors.Is(err, hostbox.ErrChanged) {
134 t.Fatal(err)
135 }
136 if _, _, err = a.Launch(ctx, "foreign.example"); !errors.Is(err, hostbox.ErrChanged) {
137 t.Fatal(err)
138 }
139 if _, err = a.HeldWork(ctx, "foreign.example", "held-config", captured); !errors.Is(err, hostbox.ErrChanged) {
140 t.Fatal(err)
141 }
142 if reads != previous {
143 t.Fatal("foreign host reached registry")
144 }
145 work, err := a.HeldWork(ctx, adminTokenHost, "held-release", captured)
146 if err != nil {
147 t.Fatal(err)
148 }
149 f.verify = hostbox.ErrChanged
150 checkAll(hostbox.ErrChanged)
151 if err = work.With(ctx, hostwrite.Identity{}, func() error { t.Fatal("effect after lost identity"); return nil }); !errors.Is(err, hostbox.ErrChanged) {
152 t.Fatal(err)
153 }
154 if reads != previous {
155 t.Fatal("unverified identity reached registry")
156 }
157}
158
159func TestPlan10G2LaunchReadOnlyAndSameReport(t *testing.T) {
160 f := newAdminTokenFixture(t, "box-1")
161 source := claimMoveLaunchSource(f.backend)
162 if _, _, err := source(t.Context(), f.rt); err == nil {
163 t.Fatal("missing token accepted")
164 }
165 if _, err := os.Stat(f.envPath()); !errors.Is(err, os.ErrNotExist) {
166 t.Fatal("read created token", err)
167 }
168 t.Setenv("CARLOS_SECRETS_KEY", base64.StdEncoding.EncodeToString(make([]byte, 32)))
169 if err := os.WriteFile(f.envPath(), []byte("CARLOS_ADMIN_TOKEN=existing\nSECRET=value\nCARLOS_MOVE_PROBE=1\n"), 0600); err != nil {
170 t.Fatal(err)
171 }
172 before, err := os.ReadFile(f.envPath())
173 if err != nil {
174 t.Fatal(err)
175 }
176 launch, report, err := source(t.Context(), f.rt)
177 if err != nil {
178 t.Fatal(err)
179 }
180 after, _ := os.ReadFile(f.envPath())
181 if !reflect.DeepEqual(before, after) {
182 t.Fatal("launch read wrote environment")
183 }
184 env := map[string]string{}
185 for _, kv := range launch.Env {
186 k, v, ok := strings.Cut(kv, "=")
187 if ok {
188 env[k] = v
189 }
190 }
191 if env["CARLOS_CONTROL_SOCKET"] != f.rt.Socket+".control" {
192 t.Fatal("wrong role environment")
193 }
194 if _, ok := env["CARLOS_MOVE_PROBE"]; ok {
195 t.Fatal("probe-only input included in pin")
196 }
197 key, err := moves.ReportKeyFromSecrets(os.Getenv("CARLOS_SECRETS_KEY"))
198 if err != nil {
199 t.Fatal(err)
200 }
201 if !reflect.DeepEqual(report, moves.ReportEnv(key, env)) {
202 t.Fatal("report is not the returned launch environment")
203 }
204}
205
206// Count dispatches while using main's concrete move pin checker.
207type plan10InputOwner struct {
208 *movesProbeOwners
209 calls int
210}
211
212func (m *plan10InputOwner) InputPolicy(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
213 m.calls++
214 return m.movesProbeOwners.InputPolicy(ctx, c, s, id)
215}
216
217func TestPlan10CommittedInputDispatch(t *testing.T) {
218 reader := &mpReader{rec: mpRecord()}
219 reader.rec.Move.State = provision.MoveCommitted
220 reader.rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin)
221 owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: reader.read}}
222 serving := 0
223 legacy := func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
224 serving++
225 return hostbox.ErrChanged
226 }
227 out := ClaimOwnerBindings{InputPolicy: legacy, MovesPolicy: MovesClaimPolicy(reader.read), Moves: ClaimMovesOwners{ClaimKindOwners: ClaimKindOwners{Inputs: owner}}}
228 bindProductionClaimInputPolicy(func(string) string { return "1" }, &out, reader.read)
229 policy := hostbox.Policy{Inputs: out.InputPolicy, Moves: out.MovesPolicy, Held: func(context.Context, string) (string, error) { return "", nil }}
230 c := mpClaim(hostclaim.KindLanding, hostclaim.PhaseLandingCommitted)
231 c.Holder.WriterBox = mpTarget
232 s := mpSelection(c, "spawn")
233 id := pinnedLaunchIdentity(reader.rec.Move.Inputs)
234 if err := hostbox.WakePhase(c, mpTarget, false); err != nil {
235 t.Fatal("cold wake phase", err)
236 }
237 if err := policy.Check(t.Context(), c, s, id); err != nil {
238 t.Fatal("matching committed cold-spawn inputs", err)
239 }
240 for _, bad := range []hostwrite.Identity{{Config: "different", Release: id.Release}, {Config: id.Config, Release: "different"}} {
241 if err := policy.Check(t.Context(), c, s, bad); err == nil {
242 t.Fatal("mismatching committed inputs accepted")
243 }
244 }
245 if owner.calls != 3 || serving != 0 {
246 t.Fatalf("moves=%d serving=%d", owner.calls, serving)
247 }
248 for _, kind := range []string{hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring} {
249 c.Op.Kind = kind
250 s.Kind = kind
251 before := owner.calls
252 _ = out.InputPolicy(t.Context(), c, s, id)
253 if owner.calls != before+1 || serving != 0 {
254 t.Fatalf("%s fell back", kind)
255 }
256 }
257 c.Op.Kind = hostclaim.KindServing
258 s.Kind = c.Op.Kind
259 _ = out.InputPolicy(t.Context(), c, s, id)
260 if serving != 1 {
261 t.Fatal("serving lost own check")
262 }
263 c.Op.Kind = hostclaim.KindLanding
264 s.Kind = c.Op.Kind
265 missing := ClaimOwnerDispatch{Serving: ClaimKindOwners{InputPolicy: legacy}}
266 if err := missing.InputPolicy(t.Context(), c, s, id); err == nil || serving != 1 {
267 t.Fatal("missing move owner fell back")
268 }
269 for _, off := range []string{"CARLOS_HOST_CLAIM", "CARLOS_MOVES_PHASE1"} {
270 legacyOut := ClaimOwnerBindings{InputPolicy: legacy, MovesPolicy: MovesClaimPolicy(reader.read), Moves: out.Moves}
271 bindProductionClaimInputPolicy(func(k string) string {
272 if k == off {
273 return ""
274 }
275 return "1"
276 }, &legacyOut, reader.read)
277 if reflect.ValueOf(legacyOut.InputPolicy).Pointer() != reflect.ValueOf(legacy).Pointer() {
278 t.Fatal("flag-off callback changed")
279 }
280 if err := legacyOut.MovesPolicy(t.Context(), c, s, id); err == nil {
281 t.Fatal("flag-off committed spawn granted")
282 }
283 }
284}
285
286type plan10PinnedInputs struct {
287 plan9D2Inputs
288 identity hostwrite.Identity
289}
290
291func (p plan10PinnedInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (ClaimLaunchInputs, error) {
292 in, err := p.plan9D2Inputs.Select(ctx, rt, o, policy)
293 in.Identity = p.identity
294 return in, err
295}
296
297func TestPlan10CommittedColdAdmissionInputs(t *testing.T) {
298 for _, mismatch := range []bool{false, true} {
299 t.Run(fmt.Sprint(mismatch), func(t *testing.T) {
300 f := newRecoveryFixture(t)
301 f.recordPrelaunch = true
302 ctx := t.Context()
303 o, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error {
304 c.Op = hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: hostclaim.PhaseLandingCommitted, Exec: "box-1", Participants: []string{"box-0", "box-1"}, Intent: map[string]string{"to": "ie", "source": "box-0", "target": "box-1"}}
305 return nil
306 })
307 if err != nil {
308 t.Fatal(err)
309 }
310 rec := &provision.Record{Host: recoveryHost, Move: &provision.MoveState{ID: "m1", Incarnation: o.Claim.Holder.Incarnation, State: provision.MoveCommitted, Inputs: mpPins()}}
311 rec.Move.Inputs.EnvReport = "env2:sha256:report" // a v2 pin (moves.CurrentEnvPin)
312 read := MovesRecordReader(func(context.Context, string, string, string) (*provision.Record, error) { return rec, nil })
313 owner := &plan10InputOwner{movesProbeOwners: &movesProbeOwners{Records: read}}
314 bindings := ClaimOwnerBindings{InputPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
315 t.Fatal("serving fallback")
316 return nil
317 }, Moves: ClaimMovesOwners{ClaimKindOwners: ClaimKindOwners{Inputs: owner}}}
318 bindProductionClaimInputPolicy(func(string) string { return "1" }, &bindings, read)
319 policy := hostbox.Policy{Inputs: bindings.InputPolicy, Moves: bindings.MovesPolicy, Held: func(context.Context, string) (string, error) { return "", nil }}
320 c := f.controller("boot-1", "controller-1", 4101, "start-1")
321 id := pinnedLaunchIdentity(rec.Move.Inputs)
322 if mismatch {
323 id.Release = "different"
324 }
325 c.wake.Inputs = plan10PinnedInputs{plan9D2Inputs{recoveryInputs{f}}, id}
326 c.wake.Continue = hostclaim.SessionContinues
327 c.wake.Operations = &plan9D2Attempts{}
328 c.wake.ProbeReady = func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil }
329 source := hostbox.WorkSource{Guard: f.guard, Box: "box-1", Region: "ie", Now: func() time.Time { return f.now }, Policy: policy.Check, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }}
330 writer := hostbox.Writer{Claims: f.claims, Box: "box-1", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }}
331 c.wake.mu.Lock()
332 c.wake.pending = nil
333 c.wake.mu.Unlock()
334 runtime, err := c.wake.Build(ctx, source, writer)
335 if err != nil {
336 t.Fatal(err)
337 }
338 live, cancel := context.WithCancel(ctx)
339 defer cancel()
340 done := make(chan struct{})
341 go func() { <-live.Done(); close(done) }()
342 c.wake.mu.Lock()
343 c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
344 c.wake.mu.Unlock()
345 admitted, err := runtime.Admit(ctx, registry.Route{Host: recoveryHost}, o, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second})
346 if mismatch {
347 if err == nil {
348 t.Fatal("mismatching committed cold admission accepted")
349 }
350 } else if err != nil {
351 t.Fatal(err)
352 } else if admitted.Claim.Holder.Session <= o.Claim.Holder.Session || admitted.Claim.Holder.Generation == o.Claim.Holder.Generation {
353 t.Fatal("not a cold admission")
354 }
355 if owner.calls == 0 {
356 t.Fatal("admission bypassed moves input check")
357 }
358 })
359 }
360}
361