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 "fmt"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 execlease "github.com/carlosframework/platform/internal/hostexec"
18 "github.com/carlosframework/platform/internal/hostfiles"
19 "github.com/carlosframework/platform/internal/hostguard"
20 "github.com/carlosframework/platform/internal/hostrelocate"
21 "github.com/carlosframework/platform/internal/hostwrite"
22 "github.com/carlosframework/platform/internal/moves"
23 "github.com/carlosframework/platform/internal/registry"
24 "github.com/carlosframework/platform/internal/release"
25)
26
27// probeFilesFixture is a relocation target at relocating/probing reached
28// through a real build, a real install and the claim's own transitions
29// (the prepared authority as ToInstalled journals it, RecordLocalInstall,
30// ToProbing), with the real C8 provider the production source opens over
31// the box's data directory, registry, claim store (Plan 15's origin
32// proof) and data bucket (merge round 6), and
33// the relocation's wake inputs bound to it.
34type probeFilesFixture struct {
35 *installFixture
36 receipt hostclaim.LocalInstall
37 inputs *RelocationWakeInputs
38 // replaced is what the replaced target left when the fixture replaced
39 // it (newReplacedProbeFilesFixture): its receipt and its install
40 // journal's bytes, at P's build stamp.
41 replaced struct {
42 receipt hostclaim.LocalInstall
43 journal []byte
44 }
45}
46
47func newProbeFilesFixture(t *testing.T, opts buildOptions) *probeFilesFixture {
48 t.Helper()
49 f := newProbeFilesInstalled(t, opts)
50 f.installAndProbe()
51 f.bindProvider()
52 return f
53}
54
55// newProbeFilesInstalled is the fixture at installed, P built and its
56// prepared authority journalled, before any local install. Claim Plan
57// 15's origin record is on, as the provider's origin proof
58// (ClaimOriginProof) needs: every admission through the claim's CAS, the
59// source's g1 and the probe's P, is recorded.
60func newProbeFilesInstalled(t *testing.T, opts buildOptions) *probeFilesFixture {
61 t.Helper()
62 t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
63 return &probeFilesFixture{installFixture: newInstallFixtureWith(t, opts)}
64}
65
66// installAndProbe is the executor's local install at the current install
67// stamp, its receipt journalled, and its entry into probing.
68func (f *probeFilesFixture) installAndProbe() {
69 f.t.Helper()
70 var err error
71 if f.receipt, err = f.install(); err != nil {
72 f.t.Fatal(err)
73 }
74 f.update(hostclaim.RecordLocalInstall("rel-1", f.receipt))
75 f.update(hostclaim.ToProbing("rel-1", time.Now()))
76}
77
78// newReplacedProbeFilesFixture replaces a terminated target that had made
79// its local install, at phase (installed, or probing before P's first
80// admission), as hostclaim.ReplaceTarget does for both: install(c, next,
81// P, installed) names the replacement box as the executor, keeps P and its
82// prepared authority, issues a new install stamp (the CAS's generation)
83// and drops the receipt; ToInstalled does not run again. The replacement,
84// dst2, is a fresh box with no guard record: its participant's local
85// install activates the installation at the new stamp, makes the install,
86// records it and enters probing (participate). The replaced target's
87// receipt and journal bytes are kept for the negatives.
88func newReplacedProbeFilesFixture(t *testing.T, opts buildOptions, phase string) *probeFilesFixture {
89 t.Helper()
90 f := newProbeFilesInstalled(t, opts)
91 var err error
92 if f.replaced.receipt, err = f.install(); err != nil {
93 t.Fatal(err)
94 }
95 f.update(hostclaim.RecordLocalInstall("rel-1", f.replaced.receipt))
96 if phase == hostclaim.PhaseRelocatingProbing {
97 f.update(hostclaim.ToProbing("rel-1", time.Now()))
98 }
99 if f.read().Claim.Op.Phase != phase {
100 t.Fatalf("the replaced target is at %s, not %s", f.read().Claim.Op.Phase, phase)
101 }
102 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
103 if f.replaced.journal, err = os.ReadFile(path); err != nil {
104 t.Fatal(err)
105 }
106 build := f.read().Claim.Holder.Stamp
107 next := hostbox.Actor{Box: "dst2", Enrollment: strings.Repeat("b", 32), Region: "ie", Revision: 4, Binding: "4"}
108 f.update(func(c *hostclaim.Claim) error {
109 if hostclaim.PreparedOpened(c) {
110 return errors.New("P was opened")
111 }
112 stamp := c.Gen + 1 // hostclaim install(): the CAS's resulting gen
113 c.Op.Exec, c.Op.Phase = next.Box, hostclaim.PhaseInstalled
114 c.Op.Intent["target_box"], c.Op.Intent["target_enrollment"] = next.Box, next.Enrollment
115 c.Op.Participants = []string{"src", next.Box}
116 c.Op.Journal["exec_ack."+next.Box] = `{"op":"rel-1"}`
117 c.Holder.Stamp = stamp
118 c.Op.Journal["install_stamp"] = strconv.FormatUint(stamp, 10)
119 delete(c.Op.Journal, "local_install")
120 return nil
121 })
122 c := f.read().Claim
123 auth, err := preparedAuthority(c)
124 if err != nil || auth.InstallStamp != build || c.Holder.Stamp == build {
125 t.Fatalf("the replacement keeps P's authority at the build stamp %d under a new stamp %d: %+v %v", build, c.Holder.Stamp, auth, err)
126 }
127 f.onBox(claimCensusBox(t), next)
128 f.participate()
129 f.bindProvider()
130 return f
131}
132
133// participate drives this box's relocation participant from installed:
134// its first pass makes the local install (InstallLocal, which activates a
135// replacement's installation at its new stamp; nothing else does) and
136// journals the receipt, its second enters probing.
137func (f *probeFilesFixture) participate() {
138 f.t.Helper()
139 e := f.effects()
140 incarnation := f.read().Claim.Holder.Incarnation
141 e.Participation = func(context.Context, hostclaim.Observed) (execlease.State, error) {
142 return execlease.State{Binding: execlease.Binding{Host: buildHost, Incarnation: incarnation, Op: "rel-1"}}, nil
143 }
144 p := hostrelocate.Participant{Claims: f.claims, Box: f.actor.Box, Enrollment: f.actor.Enrollment, Effects: e, Now: time.Now}
145 o, err := p.Step(f.t.Context(), buildHost)
146 if err != nil || o.Claim.Op.Journal["local_install"] == "" {
147 f.t.Fatalf("the participant's install: %v", err)
148 }
149 if o, err = p.Step(f.t.Context(), buildHost); err != nil || o.Claim.Op.Phase != hostclaim.PhaseRelocatingProbing {
150 f.t.Fatalf("the participant's entry into probing: %v", err)
151 }
152 if err := json.Unmarshal([]byte(o.Claim.Op.Journal["local_install"]), &f.receipt); err != nil {
153 f.t.Fatal(err)
154 }
155 f.obs = o
156}
157
158// bindProvider opens the real C8 provider the production source opens
159// and binds the relocation's wake inputs to it.
160func (f *probeFilesFixture) bindProvider() {
161 t := f.t
162 ctx := t.Context()
163 src := &movesFilesSource{DataDir: f.r.DataDir, Routes: f.reg, newFiles: func(context.Context, string, string) (release.Store, error) {
164 return bucketStore{b: f.bucket, bucket: "data"}, nil
165 }}
166 c8, err := src.Open(ctx, f.claims, f.bucket, hostbox.Actor{Box: f.actor.Box, Region: "ie", Bucket: "data", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}})
167 if err != nil {
168 t.Fatal(err)
169 }
170 seq := 0
171 f.inputs = &RelocationWakeInputs{
172 Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
173 return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/opt/carlos/live/" + buildHost, ExecutableSHA256: strings.Repeat("b", 64), Version: "v1"}, nil
174 },
175 FilesManifest: c8, Markers: plan9D5Markers{f.bucket}, Claims: f.claims, FilesStore: f.files, Data: f.bucket, Bucket: "data", Region: "ie", Box: f.actor.Box,
176 NewID: func() string { seq++; return fmt.Sprintf("fresh%d", seq) }, Now: time.Now, Releases: &roReleases{}}
177}
178
179func (f *probeFilesFixture) update(tr func(*hostclaim.Claim) error) {
180 f.t.Helper()
181 o, err := hostclaim.Update(f.t.Context(), f.claims, f.read(), tr)
182 if err != nil {
183 f.t.Fatal(err)
184 }
185 f.obs = o
186}
187
188// probe runs the relocation's first probe up to the app's launch: Select
189// on the host's row, the attempt counted and the admission applied with
190// the claim's own CAS, then Materialize under the start's work, which
191// establishes the files manifest through the real provider. Before
192// Materialize the launch source's key mint is stood for by env, written
193// over the nested env file the install placed.
194func (f *probeFilesFixture) probe(env string) (ClaimLaunchInputs, error) {
195 f.t.Helper()
196 ctx := f.t.Context()
197 o := f.read()
198 row, err := f.reg.Get(buildHost)
199 if err != nil || row == nil {
200 f.t.Fatalf("row: %v", err)
201 }
202 in, err := f.inputs.Select(ctx, *row, o, roProbe)
203 if err != nil {
204 return in, err
205 }
206 counted, err := hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt("rel-1", f.actor.Box))
207 if err != nil {
208 f.t.Fatal(err)
209 }
210 in.Admission.ObservedSession, in.Admission.ObservedGeneration = counted.Claim.Holder.Session, counted.Claim.Holder.Generation
211 admitted, err := hostclaim.Update(ctx, f.claims, counted, hostclaim.AdmitWriter(in.Admission))
212 if err != nil {
213 f.t.Fatal(err)
214 }
215 if err := os.WriteFile(f.paths().EnvPath, []byte(env), 0o600); err != nil {
216 f.t.Fatal(err)
217 }
218 return in, f.inputs.Materialize(ctx, admitted, in, passWork{})
219}
220
221// established is P's current files manifest and whether its prepared
222// sequence-0 version is still readable by the exact version the install
223// bound.
224func (f *probeFilesFixture) established() (hostfiles.Manifest, preparedManifest) {
225 f.t.Helper()
226 location := f.read().Claim.Holder.Location
227 m, found, err := hostfiles.Load(f.t.Context(), f.files, movesFilesPrefix(location, "p1"))
228 if err != nil || !found {
229 f.t.Fatalf("P's manifest: found=%v %v", found, err)
230 }
231 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
232 raw, err := os.ReadFile(path)
233 if err != nil {
234 f.t.Fatal(err)
235 }
236 var j relocationInstallJournal
237 if err := json.Unmarshal(raw, &j); err != nil {
238 f.t.Fatal(err)
239 }
240 var pm preparedManifest
241 if err := json.Unmarshal([]byte(j.Steps[relocationStepFilesManifest]), &pm); err != nil {
242 f.t.Fatal(err)
243 }
244 return m, pm
245}
246
247const firstProbeEnv = installEnv + "CARLOS_INSTANCE_KEY=minted-by-the-launch-source\n"
248
249// Merge round 6, findings 2 and 3: relocation's first probe of a host with
250// a database establishes its files manifest through main's real C8
251// provider. The provider reads the env file the launch source minted into
252// the nested instance directory (never a stale flat <host>.env), and
253// reads P's prepared manifest by the binding relocation's install
254// journalled (no moves op journal exists), replacing it by compare-and-
255// swap: P's entries carried, the env file's bytes on disk, the sequence-0
256// version still readable at the exact version the install bound.
257func TestRelocationFirstProbeEstablishesThroughTheC8Provider(t *testing.T) {
258 f := newProbeFilesFixture(t, buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}})
259 stale := filepath.Join(f.r.DataDir, buildHost+".env")
260 if err := os.WriteFile(stale, []byte("STALE=flat\n"), 0o600); err != nil {
261 t.Fatal(err)
262 }
263 if _, err := os.Stat(moves.JournalFile(f.r.DataDir, buildHost, "p1")); !errors.Is(err, os.ErrNotExist) {
264 t.Fatalf("a moves op journal exists for P: %v", err)
265 }
266 in, err := f.probe(firstProbeEnv)
267 if err != nil {
268 t.Fatalf("the first probe: %v", err)
269 }
270 if !in.Prepared || in.Empty {
271 t.Fatalf("prepared=%v empty=%v", in.Prepared, in.Empty)
272 }
273 m, pm := f.established()
274 if m.Platform[hostfiles.EnvItem].SHA256 != hostfilesSum([]byte(firstProbeEnv)) {
275 t.Fatalf("the manifest carries env %s, not the nested env file's bytes (stale flat %s)", m.Platform[hostfiles.EnvItem].SHA256, hostfilesSum([]byte("STALE=flat\n")))
276 }
277 if _, ok := m.Entries["public/index.html"]; !ok || m.Seq == 0 {
278 t.Fatalf("P's entries were not carried into the establishment: %+v", m)
279 }
280 exact, err := f.bucket.GetVersion(t.Context(), "data", pm.Key, pm.Version)
281 if err != nil || hostfilesSum(exact) != pm.SHA256 {
282 t.Fatalf("the prepared version the install bound: %v", err)
283 }
284 // A retry of the same establishment finds its own revision.
285 if err := f.inputs.FilesManifest.Establish(t.Context(), f.read(), in, passWork{}); err != nil {
286 t.Fatalf("a retry of the establishment: %v", err)
287 }
288}
289
290// Merge round 6, finding 4: an empty database still consumes P's files.
291// The relocation's first probe of a host whose pin evidences an empty
292// database (in.Empty) establishes from P's prepared manifest, which
293// relocation always builds, by compare-and-swap over it: no create-only
294// commit collides with it, and P's files and platform items are carried.
295func TestRelocationEmptyDatabaseProbeConsumesPsFiles(t *testing.T) {
296 f := newProbeFilesFixture(t, buildOptions{DB: false, Files: map[string]string{"uploads/a.txt": "kept"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}})
297 in, err := f.probe(firstProbeEnv)
298 if err != nil {
299 t.Fatalf("the empty database's first probe: %v", err)
300 }
301 if !in.Empty || in.Prepared {
302 t.Fatalf("prepared=%v empty=%v", in.Prepared, in.Empty)
303 }
304 m, _ := f.established()
305 if _, ok := m.Entries["uploads/a.txt"]; !ok || m.Platform[hostfiles.WakeItem].SHA256 == "" || m.Platform[hostfiles.EnvItem].SHA256 != hostfilesSum([]byte(firstProbeEnv)) {
306 t.Fatalf("P's files were not consumed: %+v", m)
307 }
308}
309
310// Merge round 6, finding 3: the reader is chosen by the claim's op kind,
311// and relocation's binding is held as strongly as a move's. A moves op
312// journal for P is never read for a relocating op; the install journal
313// must be the one the claim's receipt names by digest; its binding must
314// carry the prepared authority's digest; and with no install journal the
315// probe refuses rather than reading the absence as an empty start.
316func TestRelocationPreparedBindingIsRelocationsOwn(t *testing.T) {
317 opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
318 installJournal := func(f *probeFilesFixture) string {
319 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
320 return path
321 }
322 t.Run("no install journal", func(t *testing.T) {
323 f := newProbeFilesFixture(t, opts)
324 if err := os.Remove(installJournal(f)); err != nil {
325 t.Fatal(err)
326 }
327 if _, err := f.probe(firstProbeEnv); err == nil {
328 t.Fatal("a probe with no install journal established")
329 }
330 if m, _, _ := hostfiles.Load(t.Context(), f.files, movesFilesPrefix(f.read().Claim.Holder.Location, "p1")); m.Seq != 0 {
331 t.Fatalf("P's manifest was replaced: %+v", m)
332 }
333 })
334 t.Run("an install journal the receipt does not name", func(t *testing.T) {
335 f := newProbeFilesFixture(t, opts)
336 path := installJournal(f)
337 raw, err := os.ReadFile(path)
338 if err != nil {
339 t.Fatal(err)
340 }
341 if err := os.WriteFile(path, append(raw, ' '), 0o600); err != nil {
342 t.Fatal(err)
343 }
344 if _, err := f.probe(firstProbeEnv); !errors.Is(err, hostbox.ErrChanged) {
345 t.Fatalf("a changed install journal: %v", err)
346 }
347 })
348 t.Run("a binding that is not the prepared authority's", func(t *testing.T) {
349 f := newProbeFilesFixture(t, opts)
350 f.update(func(c *hostclaim.Claim) error {
351 auth, err := preparedAuthority(c)
352 if err != nil {
353 return err
354 }
355 auth.FilesManifestSHA256 = strings.Repeat("0", 64)
356 c.Op.Journal["prepared_authority"] = string(mustJSON(t, auth))
357 return nil
358 })
359 if _, err := f.probe(firstProbeEnv); !errors.Is(err, hostbox.ErrChanged) {
360 t.Fatalf("a binding with another digest: %v", err)
361 }
362 })
363 t.Run("a moves op journal is never read for a relocating op", func(t *testing.T) {
364 f := newProbeFilesFixture(t, opts)
365 location := f.read().Claim.Holder.Location
366 key := hostfiles.ManifestKey(movesFilesPrefix(location, "p1"))
367 // A moves op journal binding P at a version that does not exist:
368 // read, it would refuse; the relocation's own binding is read.
369 j, err := moves.ReadJournal(f.r.DataDir, buildHost, "p1")
370 if err != nil {
371 t.Fatal(err)
372 }
373 j.Mark(landStepFilesManifest, string(mustJSON(t, preparedManifest{Key: key, SHA256: strings.Repeat("1", 64), Version: "v-none", Op: "rel-1", Stamp: f.read().Claim.Holder.Stamp})))
374 if err := j.Commit(f.r.DataDir); err != nil {
375 t.Fatal(err)
376 }
377 if _, err := f.probe(firstProbeEnv); err != nil {
378 t.Fatalf("the relocation's own binding: %v", err)
379 }
380 })
381}
382
383// Merge round 7: a replacement target keeps P and its prepared authority
384// at P's build stamp while it installs under a new install stamp (at
385// installed, or in probing before P's first admission). Its first probe
386// establishes P's files through the real provider: P's manifest bound at
387// the build stamp, its own receipt and install journal at the new one.
388// The replacement's install journal records P's build stamp in its
389// files_manifest step and its own stamp as the journal's install stamp.
390func TestRelocationReplacementProbeEstablishesPsFiles(t *testing.T) {
391 opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
392 for _, phase := range []string{hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} {
393 t.Run("replaced at "+phase, func(t *testing.T) {
394 f := newReplacedProbeFilesFixture(t, opts, phase)
395 c := f.read().Claim
396 auth, _ := preparedAuthority(c)
397 if f.receipt.InstallStamp != c.Holder.Stamp || f.receipt.InstallStamp == auth.InstallStamp {
398 t.Fatalf("the replacement's receipt %+v is not at its own install stamp %d", f.receipt, c.Holder.Stamp)
399 }
400 if _, err := f.probe(firstProbeEnv); err != nil {
401 t.Fatalf("the replacement's first probe: %v", err)
402 }
403 m, pm := f.established()
404 if pm.Stamp != auth.InstallStamp || m.Platform[hostfiles.EnvItem].SHA256 != hostfilesSum([]byte(firstProbeEnv)) || m.Entries["public/index.html"].SHA256 == "" {
405 t.Fatalf("binding %+v (build stamp %d), manifest %+v", pm, auth.InstallStamp, m)
406 }
407 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
408 raw, _ := os.ReadFile(path)
409 var j relocationInstallJournal
410 if err := json.Unmarshal(raw, &j); err != nil || j.InstallStamp != c.Holder.Stamp {
411 t.Fatalf("the replacement's journal is at %d, not its install stamp %d: %v", j.InstallStamp, c.Holder.Stamp, err)
412 }
413 })
414 }
415}
416
417// Merge round 7: neither stamp stands in for the other. The replaced
418// target's install journal (P's build stamp), or its receipt, under the
419// replacement's install stamp refuses; a prepared authority whose stamp
420// is not the build stamp the install bound, or is past the install
421// stamp, refuses; and nothing is established.
422func TestRelocationReplacementRefusesTheWrongStamp(t *testing.T) {
423 opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
424 for name, tamper := range map[string]func(f *probeFilesFixture){
425 "the replaced target's install journal": func(f *probeFilesFixture) {
426 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
427 if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil {
428 f.t.Fatal(err)
429 }
430 },
431 "the replaced target's install journal and receipt": func(f *probeFilesFixture) {
432 path, _ := relocationInstallPath(f.r.DataDir, "rel-1")
433 if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil {
434 f.t.Fatal(err)
435 }
436 f.update(func(c *hostclaim.Claim) error {
437 c.Op.Journal["local_install"] = string(mustJSON(f.t, f.replaced.receipt))
438 return nil
439 })
440 },
441 "an authority at the install stamp": func(f *probeFilesFixture) {
442 f.update(func(c *hostclaim.Claim) error {
443 auth, err := preparedAuthority(c)
444 if err != nil {
445 return err
446 }
447 auth.InstallStamp = c.Holder.Stamp
448 c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth))
449 return nil
450 })
451 },
452 "an authority past the install stamp": func(f *probeFilesFixture) {
453 f.update(func(c *hostclaim.Claim) error {
454 auth, err := preparedAuthority(c)
455 if err != nil {
456 return err
457 }
458 auth.InstallStamp = c.Holder.Stamp + 1
459 c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth))
460 return nil
461 })
462 },
463 } {
464 t.Run(name, func(t *testing.T) {
465 f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseRelocatingProbing)
466 tamper(f)
467 if _, err := f.probe(firstProbeEnv); err == nil {
468 t.Fatal("established under the wrong stamp")
469 }
470 if m, _, _ := hostfiles.Load(t.Context(), f.files, movesFilesPrefix(f.read().Claim.Holder.Location, "p1")); m.Seq != 0 {
471 t.Fatalf("P's manifest was replaced: %+v", m)
472 }
473 })
474 }
475}
476
477// Astra round 8: a replacement's activation is the install's, and no
478// broader. The box that runs it must be the claim's executor and named
479// target, P must be unopened, and a stamp the guard retired is never
480// reactivated; each refusal leaves the guard as it was.
481func TestRelocationReplacementActivationRefuses(t *testing.T) {
482 opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}}
483 t.Run("a retired stamp", func(t *testing.T) {
484 f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseInstalled)
485 if err := f.a.Guard.RetireInstallation(t.Context(), buildHost, func(*hostclaim.Claim) error { return nil }); err != nil {
486 t.Fatal(err)
487 }
488 f.update(func(c *hostclaim.Claim) error {
489 c.Op.Phase = hostclaim.PhaseInstalled
490 delete(c.Op.Journal, "local_install")
491 return nil
492 })
493 if _, err := f.effects().InstallLocal(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) || !errors.Is(err, hostguard.ErrRetiredStamp) {
494 t.Fatalf("a retired stamp was reactivated: %v", err)
495 }
496 if captured, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); err == nil && captured.Stamp != 0 {
497 t.Fatalf("the retired installation is active again: %+v", captured)
498 }
499 })
500 // The claim keeps its executor and named target one box (Validate
501 // refuses them apart), so a box that is not named is one of two: the
502 // claim names another box, or the replaced box asks after the claim
503 // has named its replacement.
504 for name, tc := range map[string]struct {
505 change func(*hostclaim.Claim)
506 box string
507 }{
508 "a box the claim does not name": {change: func(c *hostclaim.Claim) { c.Op.Exec, c.Op.Intent["target_box"] = "dst3", "dst3" }},
509 "the replaced box": {change: func(*hostclaim.Claim) {}, box: "dst"},
510 } {
511 t.Run(name, func(t *testing.T) {
512 f := newReplacedNamedFixture(t, opts, tc.change)
513 if tc.box != "" {
514 f.cfg.Source.Box = tc.box
515 }
516 if _, err := f.a.relocationActivateInstalled(f.cfg)(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) {
517 t.Fatalf("activated: %v", err)
518 }
519 if _, err := f.effects().InstallLocal(t.Context(), f.read()); err == nil {
520 t.Fatal("installed")
521 }
522 if _, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); !errors.Is(err, hostguard.ErrNoRecord) {
523 t.Fatalf("a guard record was made: %v", err)
524 }
525 })
526 }
527 t.Run("probing after P opened", func(t *testing.T) {
528 f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseRelocatingProbing)
529 if _, err := f.probe(firstProbeEnv); err != nil {
530 t.Fatal(err)
531 }
532 if !hostclaim.PreparedOpened(f.read().Claim) {
533 t.Fatal("P is not opened")
534 }
535 if _, err := f.a.relocationActivateInstalled(f.cfg)(t.Context(), f.read()); !errors.Is(err, hostrelocate.ErrRefused) {
536 t.Fatalf("activated after P opened: %v", err)
537 }
538 })
539}
540
541// newReplacedNamedFixture is the replacement at installed of
542// newReplacedProbeFilesFixture on a fresh box dst2, stopped before dst2's
543// participant runs, with change applied to the claim.
544func newReplacedNamedFixture(t *testing.T, opts buildOptions, change func(*hostclaim.Claim)) *probeFilesFixture {
545 t.Helper()
546 f := newProbeFilesInstalled(t, opts)
547 next := hostbox.Actor{Box: "dst2", Enrollment: strings.Repeat("b", 32), Region: "ie", Revision: 4, Binding: "4"}
548 f.update(func(c *hostclaim.Claim) error {
549 stamp := c.Gen + 1
550 c.Op.Exec, c.Op.Phase = next.Box, hostclaim.PhaseInstalled
551 c.Op.Intent["target_box"], c.Op.Intent["target_enrollment"] = next.Box, next.Enrollment
552 c.Op.Participants = []string{"src", next.Box}
553 c.Op.Journal["exec_ack."+next.Box] = `{"op":"rel-1"}`
554 c.Holder.Stamp = stamp
555 c.Op.Journal["install_stamp"] = strconv.FormatUint(stamp, 10)
556 delete(c.Op.Journal, "local_install")
557 change(c)
558 return nil
559 })
560 f.onBox(claimCensusBox(t), next)
561 return f
562}
563