| index dccf2ec8f..97eb4d60f 100644 |
| --- a/internal/host/movesprobeowners_test.go |
| +++ b/internal/host/movesprobeowners_test.go |
| @@ -18,8 +18,10 @@ import ( |
| "github.com/carlosframework/platform/internal/genselect" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostfiles" |
| "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/instancekey" |
| "github.com/carlosframework/platform/internal/lease/leasetest" |
| "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/moves/archive" |
| @@ -135,6 +137,9 @@ func newPOFixture(t *testing.T) *poFixture { |
| if err := j.Commit(f.dataDir); err != nil { |
| t.Fatal(err) |
| } |
| + // The prepared files manifest local wrote at the flip's stamp, before |
| + // .files-gen, and its binding in the op journal (C8, Task 6). |
| + f.preparedManifestJournalledAt(poOp, poOp, 18, f.files, envPath) |
| f.route = registry.Route{Host: poHost, Kind: "instance", App: "jam", Account: "bab", Status: "landing", HoldOp: poOp, Socket: filepath.Join(f.dataDir, poHost+".sock"), DBPath: filepath.Join(f.dataDir, poHost+".db")} |
| f.writeLocal(poOp, poOp, inc) |
| pins := mpPins() |
| @@ -180,6 +185,9 @@ func newPOFixture(t *testing.T) *poFixture { |
| }, |
| NewID: func() string { f.seq++; return fmt.Sprintf("fresh%d", f.seq) }, |
| Now: func() time.Time { return time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC) }} |
| + // The C8 provider is the real one over the fixture's bucket, as the |
| + // claim assembly binds it (whole-branch fixes, item 1). |
| + f.owners.Files = f.realC8() |
| return f |
| } |
| |
| @@ -337,6 +345,164 @@ func TestProbeIsAColdAdmissionIntoThePreparedGeneration(t *testing.T) { |
| } |
| } |
| |
| +// realC8 is the production C8 provider over the fixture's bucket, data |
| +// directory and files paths, as the claim assembly binds it: the probe |
| +// owners' Files is this, not a fake (whole-branch fixes, item 1). |
| +func (f *poFixture) realC8() *movesFiles { |
| + return &movesFiles{Files: casBucketStore{b: f.bucket, bucket: "data"}, Data: f.bucket, Bucket: "data", HomeRegion: "ie", Region: f.owners.Region, Box: f.owners.Box, DataDir: f.dataDir, FilesDir: f.owners.FilesDir} |
| +} |
| + |
| +// preparedManifestJournalled builds the prepared manifest local writes for |
| +// generation at the installation's stamp and journals its binding, as |
| +// local does (movesinstall.go). |
| +func (f *poFixture) preparedManifestJournalled(generation, op string, stamp uint64) preparedManifest { |
| + f.t.Helper() |
| + _, envPath := f.owners.FilesDir(poHost) |
| + return f.preparedManifestJournalledAt(generation, op, stamp, f.files, envPath) |
| +} |
| + |
| +func (f *poFixture) preparedManifestJournalledAt(generation, op string, stamp uint64, dir, envPath string) preparedManifest { |
| + f.t.Helper() |
| + pm, err := buildPreparedManifest(f.t.Context(), f.bucket, "data", "instances/"+poHost, generation, op, stamp, dir, envPath, true, hostfiles.WakeState{V: 1, Host: poHost, Timers: []hostfiles.WakeTimer{}}) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + j, err := moves.ReadJournal(f.dataDir, poHost, generation) |
| + if err != nil { |
| + f.t.Fatal(err) |
| + } |
| + j.Mark(landStepFilesManifest, string(mustJSON(f.t, pm))) |
| + if err := j.Commit(f.dataDir); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + return pm |
| +} |
| + |
| +// The landing's first probe and its committed wake establish the admitted |
| +// generation's files manifest through the registered C8 provider before |
| +// the start check (whole-branch fixes, item 1): the probe's establishment |
| +// takes P's prepared manifest at the journalled version, stages the env |
| +// file on disk and drops the adoption record; the committed wake's copies |
| +// it into the fresh generation. A refusal from C8 stops the start, and |
| +// without a provider the wake is refused by name. |
| +func TestLandingWakesEstablishThroughC8(t *testing.T) { |
| + f := newPOFixture(t) |
| + ctx := t.Context() |
| + j, err := moves.ReadJournal(f.dataDir, poHost, poOp) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var pm preparedManifest |
| + if err := json.Unmarshal([]byte(j.Steps[landStepFilesManifest]), &pm); err != nil { |
| + t.Fatal(err) |
| + } |
| + // The launch source minted the instance key into <host>.env before the |
| + // establishment (O1'): the env file differs from the prepared item. |
| + env := []byte("CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=abc\n") |
| + if err := os.WriteFile(filepath.Join(f.dataDir, poHost+".env"), env, 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + adoption := filepath.Join(f.dataDir, poHost+instancekey.AdoptionSuffix) |
| + if err := os.WriteFile(adoption, []byte(`{"v":2}`), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + o := f.read() |
| + in, err := f.owners.Select(ctx, f.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + admitted := f.admit(o, in) |
| + if err := f.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: f}); err != nil { |
| + t.Fatalf("the probe's materialization: %v", err) |
| + } |
| + prefix := movesFilesPrefix("instances/"+poHost, poOp) |
| + m, found, err := hostfiles.Load(ctx, casBucketStore{b: f.bucket, bucket: "data"}, prefix) |
| + if err != nil || !found { |
| + t.Fatal(err, found) |
| + } |
| + if m.Seq != 1 || m.Platform[hostfiles.EnvItem].SHA256 != sha(env) || m.Entries["a.txt"].Seq != 0 { |
| + t.Fatalf("P's manifest after the probe: %+v", m) |
| + } |
| + if _, has := m.Platform[instancekey.AdoptionItem]; has { |
| + t.Fatal("the adoption item was carried") |
| + } |
| + if _, err := os.Stat(adoption); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("the adoption record remains: %v", err) |
| + } |
| + if raw, err := f.bucket.GetVersion(ctx, "data", pm.Key, pm.Version); err != nil || sha(raw) != pm.SHA256 { |
| + t.Fatalf("the prepared version: %v", err) |
| + } |
| + if err := f.owners.Publish(ctx, admitted, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + // The committed wake: the claim at landing/committed, the record |
| + // committed, a cold admission into a fresh generation whose files |
| + // manifest is copied from P's. |
| + c := *f.read().Claim |
| + c.Op.Phase = hostclaim.PhaseLandingCommitted |
| + f.putClaim(&c) |
| + f.rec.Move.State, f.rec.Move.LandingPhase = provision.MoveCommitted, "" |
| + f.route.Status, f.route.HoldOp = "ready", "" |
| + o = f.read() |
| + in, err = f.owners.Select(ctx, f.route, o, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second}) |
| + if err != nil { |
| + t.Fatalf("the committed select: %v", err) |
| + } |
| + if in.Prepared || in.Admission.NewGeneration == poOp { |
| + t.Fatalf("the committed admission: %+v", in.Admission) |
| + } |
| + admitted = f.admit(o, in) |
| + f.writeLocal(poOp, admitted.Claim.Holder.Generation, f.inc) |
| + if err := f.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: f}); err != nil { |
| + t.Fatalf("the committed materialization: %v", err) |
| + } |
| + fresh, found, err := hostfiles.Load(ctx, casBucketStore{b: f.bucket, bucket: "data"}, movesFilesPrefix("instances/"+poHost, in.Admission.NewGeneration)) |
| + if err != nil || !found { |
| + t.Fatalf("the committed wake's manifest: %v %v", err, found) |
| + } |
| + if fresh.Entries["a.txt"].Object != m.Entries["a.txt"].Object || fresh.Platform[hostfiles.EnvItem].SHA256 != sha(env) { |
| + t.Fatalf("the committed wake's manifest: %+v", fresh) |
| + } |
| + |
| + // C8's refusal stops the start: no binding in the op journal. |
| + g := newPOFixture(t) |
| + gj, err := moves.ReadJournal(g.dataDir, poHost, poOp) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + gj.Unmark(landStepFilesManifest) |
| + if err := gj.Commit(g.dataDir); err != nil { |
| + t.Fatal(err) |
| + } |
| + o = g.read() |
| + in, err = g.owners.Select(ctx, g.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + admitted = g.admit(o, in) |
| + if err := g.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: g}); err == nil || !errors.Is(err, errLandingChanged) { |
| + t.Fatalf("a probe without a prepared manifest binding: %v", err) |
| + } |
| + if m, found, _ := hostfiles.Load(ctx, casBucketStore{b: g.bucket, bucket: "data"}, movesFilesPrefix("instances/"+poHost, poOp)); !found || m.Seq != 0 { |
| + t.Fatalf("a refused establishment changed the prepared manifest: %+v %v", m, found) |
| + } |
| + // Without a provider the wake is refused by name, before anything. |
| + g.owners.Files = nil |
| + var dep *ClaimDependencyError |
| + if err := g.owners.Materialize(ctx, admitted, in, poWork{action: "spawn", f: g}); !errors.As(err, &dep) { |
| + t.Fatalf("no provider: %v", err) |
| + } |
| +} |
| + |
| +// noopFiles is a C8 provider that establishes nothing: for tests of the |
| +// runtime's wiring, where the real provider's inputs are not laid out. |
| +type noopFiles struct{} |
| + |
| +func (noopFiles) Establish(ctx context.Context, _ hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error { |
| + return work.With(ctx, in.Identity, func() error { return nil }) |
| +} |
| + |
| func mpLaunchIdentity(p *provision.MoveInputs) hostwrite.Identity { |
| return hostwrite.Identity{Config: HeldConfigIdentity(p.BundleETags, p.OverlayETag).Config, Release: p.Release} |
| } |
| @@ -491,6 +657,10 @@ func TestFirstProbeFromTheSourceGenerationIsAdmitted(t *testing.T) { |
| // The prepared marker under the writer's fence: before the request |
| // and in the store before its attempt. |
| "spawn:" + launch.Config + "|" + launch.Release, "spawn:" + launch.Config + "|" + launch.Release, |
| + // C8's establishment (item 1): the env file is still the prepared |
| + // item's and there is no adoption item, so nothing is published and |
| + // only the local effect runs under the work. |
| + "spawn:" + launch.Config + "|" + launch.Release, |
| } |
| if !reflect.DeepEqual(f.order, want) { |
| t.Fatalf("order:\n got %q\nwant %q", f.order, want) |
| @@ -679,7 +849,7 @@ func TestMovesProbeOwnersThroughTheClaimRuntime(t *testing.T) { |
| records := func(context.Context, string, string, string) (*provision.Record, error) { return rec, nil } |
| var order []string |
| launches := 0 |
| - owners := &movesProbeOwners{Claims: f.claims, Records: records, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1", DataDir: journalDir, Markers: plan9D5Markers{bucket}, |
| + owners := &movesProbeOwners{Claims: f.claims, Records: records, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1", DataDir: journalDir, Markers: plan9D5Markers{bucket}, Files: noopFiles{}, |
| ReadyPath: func(context.Context, string, string) (string, error) { return "/ready", nil }, |
| FilesDir: func(string) (string, string) { return filesDir, "" }, |
| Route: func(string) (*registry.Route, error) { rt := route; return &rt, nil }, |