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 "os"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostclaim"
12 "github.com/carlosframework/platform/internal/hostguard"
13 "github.com/carlosframework/platform/internal/hostops"
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/rootparticipation"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19func migrationFixture(t *testing.T) (rootMigration, *rootMigrationSnapshot, *int) {
20 t.Helper()
21 state := &rollbackDisk{dir: t.TempDir()}
22 exec := &rollbackDisk{dir: t.TempDir()}
23 writes := 0
24 snapshot := &rootMigrationSnapshot{Complete: true, DirectoryRevision: 7, Exclusion: "signed collecting reservation", Report: hostops.CensusReport{Box: "box", Enrollment: "en", BootID: "boot", Reservation: "offline", DirectoryRevision: 7, CapturedAt: time.Now(), Complete: true, Installations: []hostops.CensusInstallation{}, Seeds: &hostops.CensusSeeds{Journaled: []string{}, Gated: []string{}}}, Items: []rootMigrationItem{}}
25 m := rootMigration{prepareFloors: func(context.Context) error { return nil }, guardReady: func(context.Context) error { return nil }, state: state, execution: exec, identity: rootMigrationIdentity{Deployment: "deploy", Box: "box", Enrollment: "en", Generation: 1}, quiesce: func(context.Context) (func(), error) { return func() {}, nil }, observe: func(context.Context) (rootMigrationSnapshot, error) { return *snapshot, nil }, deny: func(context.Context, rootMigrationItem) error { writes++; return nil }, recover: func(context.Context, rootMigrationItem) error { writes++; return nil }, ready: func(context.Context, rootMigrationSnapshot) error { return nil }, publish: func(context.Context, hostops.CensusReport) error { writes++; return nil }}
26 return m, snapshot, &writes
27}
28
29func TestPlan14RootMigrationRefusesBeforeEffects(t *testing.T) {
30 for _, kind := range []string{"preparing", "ready", "cutting", "aborting", "reverting", "landing", "committed", "rolling-back", "stuck", "future", "admitted-before-launch", "probing", "probed", "root-journal", "reservation", "door", "late-move", "incomplete", "seed-journal", "seed-gate", "seed-unknown"} {
31 t.Run(kind, func(t *testing.T) {
32 m, s, effects := migrationFixture(t)
33 switch kind {
34 case "root-journal":
35 s.MoveJournals = 1
36 case "reservation":
37 s.MoveReservations = 1
38 case "door":
39 s.Exclusion = ""
40 case "late-move":
41 n := 0
42 m.observe = func(context.Context) (rootMigrationSnapshot, error) {
43 n++
44 if n == 2 {
45 s.Claims = []hostclaim.Claim{{Op: hostclaim.Op{ID: "late"}}}
46 }
47 return *s, nil
48 }
49 case "incomplete":
50 s.Complete = false
51 case "seed-journal":
52 s.Report.Seeds.Journaled = []string{"host"}
53 case "seed-gate":
54 s.Report.Seeds.Gated = []string{"host"}
55 case "seed-unknown":
56 s.Report.Seeds = nil
57 default:
58 s.Records = []provision.Record{{Move: &provision.MoveState{State: kind, LandingPhase: kind}}}
59 }
60 before, _ := json.Marshal(s)
61 if m.run(t.Context()) == nil {
62 t.Fatal("migration accepted active work")
63 }
64 after, _ := json.Marshal(s)
65 if kind != "late-move" && string(before) != string(after) {
66 t.Fatal("migration changed input")
67 }
68 names, _ := m.state.Names()
69 for _, n := range names {
70 if n != "migration.lock" {
71 t.Fatal("write before preflight", n)
72 }
73 }
74 if *effects != 0 {
75 t.Fatal("effects before preflight", *effects)
76 }
77 s.MoveJournals = 0
78 s.MoveReservations = 0
79 s.Exclusion = "signed collecting reservation"
80 s.Complete = true
81 s.Records = nil
82 s.Claims = nil
83 s.Report.Seeds = &hostops.CensusSeeds{}
84 m.observe = func(context.Context) (rootMigrationSnapshot, error) { return *s, nil }
85 if err := m.run(t.Context()); err != nil {
86 t.Fatal("normal move close did not unblock", err)
87 }
88 })
89 }
90}
91
92func TestPlan14RootMigrationCrashResume(t *testing.T) {
93 for _, after := range []bool{false, true} {
94 for cut := 1; cut <= 7; cut++ {
95 t.Run(string(rune('a'+cut))+map[bool]string{true: "-after", false: "-before"}[after], func(t *testing.T) {
96 m, s, _ := migrationFixture(t)
97 s.Items = []rootMigrationItem{{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "apps/bab/app/instances/a.example.json", Box: "box", Enrollment: "en", Migration: 1}, Session: 9, ClaimETag: "claim"}}
98 n := 0
99 original := m.state
100 m.state = &floorFaultStore{Store: m.state, n: &n, fail: cut, after: after}
101 err := m.run(t.Context())
102 if err == nil {
103 t.Fatal("missing crash", cut, n)
104 }
105 m.state = original
106 // DataDir is deliberately absent on replay; all retained work is root state.
107 if err := m.run(t.Context()); err != nil {
108 t.Fatal("resume", err)
109 }
110 c, err := (rootparticipation.Ledger{State: m.execution}).Census()
111 if err != nil || c.Migration != 1 {
112 t.Fatal(c, err)
113 }
114 if err := m.run(t.Context()); err != nil {
115 t.Fatal("idempotence", err)
116 }
117 m.identity.Enrollment = "other"
118 if m.run(t.Context()) == nil {
119 t.Fatal("foreign enrollment reused migration")
120 }
121 })
122 }
123 }
124}
125
126func TestPlan14RootMigrationRetainsSessionDeny(t *testing.T) {
127 state := &rollbackDisk{dir: t.TempDir()}
128 root := hostguard.NewRootAuthority(state)
129 in := hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1}
130 if err := root.RetainMigrationDeny(t.Context(), in, 9); err != nil {
131 t.Fatal(err)
132 }
133 if err := root.RetainMigrationDeny(t.Context(), in, 8); err != nil {
134 t.Fatal(err)
135 }
136 root = hostguard.NewRootAuthority(state)
137 view, err := root.View(t.Context(), in.Host, hostguard.Captured{Incarnation: "inc", Stamp: 1})
138 if err != nil {
139 t.Fatal(err)
140 }
141 if len(view.MigrationDenies) != 1 || view.MigrationDenies[0].Session != 9 {
142 t.Fatal("lost deny", view)
143 }
144}
145
146var _ rootstate.Store = (*rollbackDisk)(nil)
147var _ = errors.Is
148var _ = os.ErrNotExist
149
150func TestPlan14RootMigrationPublishesOnlyDurableCompletion(t *testing.T) {
151 m, _, _ := migrationFixture(t)
152 m.publish = func(ctx context.Context, _ hostops.CensusReport) error {
153 raw, err := m.state.Read("migration.json")
154 if err != nil {
155 return err
156 }
157 var r rootMigrationRecord
158 if json.Unmarshal(raw, &r) != nil || r.Phase != "publishing" {
159 t.Fatal("report preceded local completion")
160 }
161 if _, err := (rootparticipation.Ledger{State: m.execution}).Census(); err != nil {
162 t.Fatal("report preceded census", err)
163 }
164 return nil
165 }
166 if err := m.run(t.Context()); err != nil {
167 t.Fatal(err)
168 }
169}
170
171func TestPlan14RootMigrationPreparedBlocksGrants(t *testing.T) {
172 m, _, _ := migrationFixture(t)
173 n := 0
174 m.state = &floorFaultStore{Store: m.state, n: &n, fail: 1, after: true}
175 if err := m.run(t.Context()); err == nil {
176 t.Fatal("prepare interruption was not reached")
177 }
178 raw, err := m.execution.Read("census.json")
179 if err != nil {
180 t.Fatal("visible prepared without root admission barrier", err)
181 }
182 var c rootparticipation.Census
183 if rootparticipation.Decode(raw, &c) != nil || c.Complete || c.Pending != "migration" {
184 t.Fatal("prepared permits root grants", c)
185 }
186}
187
188func TestPlan14RootMigrationCensusCrashResume(t *testing.T) {
189 for _, after := range []bool{false, true} {
190 for _, cut := range []int{1, 2} {
191 m, _, published := migrationFixture(t)
192 execution := m.execution
193 n := 0
194 m.execution = &floorFaultStore{Store: execution, n: &n, fail: cut, after: after}
195 if err := m.run(t.Context()); err == nil {
196 t.Fatal("census fault not reached", cut, after)
197 }
198 if *published != 0 {
199 t.Fatal("published despite failed census sync")
200 }
201 raw, err := execution.Read("census.json")
202 if err == nil {
203 var c rootparticipation.Census
204 if rootparticipation.Decode(raw, &c) != nil || c.Complete || c.Pending != "migration" {
205 t.Fatal("failed census granted", cut, after, c)
206 }
207 } else if cut != 1 || after || !errors.Is(err, os.ErrNotExist) {
208 t.Fatal(err)
209 }
210 m.execution = execution
211 if err := m.run(t.Context()); err != nil {
212 t.Fatal("census retry", cut, after, err)
213 }
214 c, err := (rootparticipation.Ledger{State: execution}).Census()
215 if err != nil || !c.Complete || *published != 1 {
216 t.Fatal("retry incomplete", c, err, *published)
217 }
218 }
219 }
220}
221
222func TestPlan14RootMigrationStartupVerification(t *testing.T) {
223 m, _, _ := migrationFixture(t)
224 root := hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()})
225 if err := m.run(t.Context()); err != nil {
226 t.Fatal(err)
227 }
228 if err := verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root); err != nil {
229 t.Fatal("matching startup", err)
230 }
231 raw, err := m.state.Read("migration.json")
232 if err != nil {
233 t.Fatal(err)
234 }
235 for _, attack := range []string{"missing", "pending", "enrollment", "schema", "census"} {
236 t.Run(attack, func(t *testing.T) {
237 if err := m.state.Write("migration.json", raw); err != nil {
238 t.Fatal(err)
239 }
240 if err := m.census(true); err != nil {
241 t.Fatal(err)
242 }
243 var r rootMigrationRecord
244 if json.Unmarshal(raw, &r) != nil {
245 t.Fatal("fixture")
246 }
247 switch attack {
248 case "missing":
249 if err := m.state.Remove("migration.json"); err != nil {
250 t.Fatal(err)
251 }
252 case "pending":
253 r.Phase = "prepared"
254 case "enrollment":
255 r.Identity.Enrollment = "other"
256 case "schema":
257 r.Schema++
258 case "census":
259 c, _ := (rootparticipation.Ledger{State: m.execution}).Census()
260 c.Complete = false
261 c.Pending = "migration"
262 b, _ := json.Marshal(c)
263 if err := m.execution.Write("census.json", b); err != nil {
264 t.Fatal(err)
265 }
266 }
267 if attack != "missing" {
268 if err := m.write("migration.json", r); err != nil {
269 t.Fatal(err)
270 }
271 }
272 if verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root) == nil {
273 t.Fatal("unsafe startup")
274 }
275 })
276 }
277}
278
279func fixtureWithoutRootMigrationCensus(t *testing.T) {
280 t.Helper()
281 oldOpen, oldShapes := openClaimActivationState, productionClaimUnitShapes
282 t.Cleanup(func() { openClaimActivationState, productionClaimUnitShapes = oldOpen, oldShapes })
283 openClaimActivationState = func(string, bool, bool) (rootstate.Store, error) { return nil, os.ErrNotExist }
284 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
285 original := openRootMigrationParticipation
286 openRootMigrationParticipation = func() (rootparticipation.Ledger, bool, error) { return rootparticipation.Ledger{}, false, nil }
287 t.Cleanup(func() { openRootMigrationParticipation = original })
288}
289
290func TestPlan14RootMigrationCompletedPreservesRevisions(t *testing.T) {
291 m, _, effects := migrationFixture(t)
292 if err := m.run(t.Context()); err != nil {
293 t.Fatal(err)
294 }
295 c, err := (rootparticipation.Ledger{State: m.execution}).Census()
296 if err != nil {
297 t.Fatal(err)
298 }
299 c.Revision = 42
300 c.LaunchRevision = 17
301 raw, _ := json.Marshal(c)
302 if err := m.execution.Write("census.json", raw); err != nil {
303 t.Fatal(err)
304 }
305 before := *effects
306 m.ready = func(context.Context, rootMigrationSnapshot) error {
307 t.Fatal("completed migration re-ran readiness")
308 return nil
309 }
310 if err := m.run(t.Context()); err != nil {
311 t.Fatal(err)
312 }
313 after, _ := m.execution.Read("census.json")
314 if string(raw) != string(after) || *effects != before {
315 t.Fatal("completed migration rewrote state or republished")
316 }
317}
318func TestPlan14RootMigrationPreparedReobserves(t *testing.T) {
319 m, s, _ := migrationFixture(t)
320 s.Items = []rootMigrationItem{{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1}, Session: 9, ClaimETag: "first"}}
321 m.recover = func(context.Context, rootMigrationItem) error { return os.ErrInvalid }
322 if m.run(t.Context()) == nil {
323 t.Fatal("missing interruption")
324 }
325 s.Items[0].Session = 10
326 s.Items[0].ClaimETag = "second"
327 seen := map[uint64]bool{}
328 m.deny = func(_ context.Context, i rootMigrationItem) error { seen[i.Session] = true; return nil }
329 m.recover = func(context.Context, rootMigrationItem) error { return nil }
330 if err := m.run(t.Context()); err != nil {
331 t.Fatal("late claim wedged migration", err)
332 }
333 if !seen[9] || !seen[10] {
334 t.Fatal("deny coverage shrank", seen)
335 }
336}
337func TestPlan14RootMigrationGuardBeforeCensus(t *testing.T) {
338 m, _, _ := migrationFixture(t)
339 m.guardReady = func(context.Context) error { return os.ErrPermission }
340 if m.run(t.Context()) == nil {
341 t.Fatal("unarmed migration accepted")
342 }
343 if _, err := m.execution.Read("census.json"); !os.IsNotExist(err) {
344 t.Fatal("pending census before guard", err)
345 }
346}
347