| new file mode 100644 |
| index 000000000..712f8f407 |
| --- /dev/null |
| +++ b/internal/host/movesfiles_test.go |
| @@ -0,0 +1,567 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "sync" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostfiles" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/lease/leasetest" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// casBucketStore is a release.Store over one leasetest bucket: current |
| +// reads with the latest version id as the etag, create-only and |
| +// compare-and-swap writes. It stands in for the S3 store the production |
| +// provider writes the data bucket through. |
| +type casBucketStore struct { |
| + b *leasetest.VersionedBucket |
| + bucket string |
| +} |
| + |
| +func (s casBucketStore) Get(_ context.Context, key string) ([]byte, string, error) { |
| + body, ok := s.b.Current(s.bucket, key) |
| + if !ok { |
| + return nil, "", os.ErrNotExist |
| + } |
| + return body, s.b.LatestID(s.bucket, key), nil |
| +} |
| + |
| +func (s casBucketStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { |
| + if ifMatch == "" { |
| + id, err := s.b.PutIfAbsent(ctx, s.bucket, key, body) |
| + if errors.Is(err, lease.ErrExists) { |
| + return "", release.ErrCASConflict |
| + } |
| + return id, err |
| + } |
| + if s.b.LatestID(s.bucket, key) != ifMatch { |
| + return "", release.ErrCASConflict |
| + } |
| + return s.b.Put(s.bucket, key, body), nil |
| +} |
| + |
| +func (s casBucketStore) List(ctx context.Context, prefix string) ([]string, error) { |
| + vs, err := s.b.ListVersions(ctx, s.bucket, prefix) |
| + if err != nil { |
| + return nil, err |
| + } |
| + var out []string |
| + for _, v := range vs { |
| + if v.IsLatest && !v.DeleteMarker { |
| + out = append(out, v.Key) |
| + } |
| + } |
| + return out, nil |
| +} |
| + |
| +func (casBucketStore) ListDirs(context.Context, string) ([]string, error) { |
| + return nil, errors.New("test: not implemented") |
| +} |
| +func (casBucketStore) Delete(context.Context, string) error { |
| + return errors.New("test: not implemented") |
| +} |
| + |
| +// recordingReader records every exact-version read. |
| +type recordingReader struct { |
| + lease.VersionedStore |
| + mu sync.Mutex |
| + reads []string // key@version |
| +} |
| + |
| +func (r *recordingReader) GetVersion(ctx context.Context, bucket, key, version string) ([]byte, error) { |
| + r.mu.Lock() |
| + r.reads = append(r.reads, key+"@"+version) |
| + r.mu.Unlock() |
| + return r.VersionedStore.GetVersion(ctx, bucket, key, version) |
| +} |
| + |
| +type filesFixture struct { |
| + t *testing.T |
| + bucket *leasetest.VersionedBucket |
| + data *recordingReader |
| + files casBucketStore |
| + dir string |
| + f *movesFiles |
| + // work records whether the start's work ran and can refuse. |
| + workRan int |
| + workErr error |
| +} |
| + |
| +const ( |
| + filesHost = "shop.example.com" |
| + filesLoc = "instances/" + filesHost + "/" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" |
| +) |
| + |
| +func (x *filesFixture) With(_ context.Context, _ hostwrite.Identity, fn func() error) error { |
| + x.workRan++ |
| + if x.workErr != nil { |
| + return x.workErr |
| + } |
| + return fn() |
| +} |
| + |
| +func newFilesFixture(t *testing.T) *filesFixture { |
| + t.Helper() |
| + b := leasetest.NewVersionedBucket() |
| + b.AddBucket("data") |
| + x := &filesFixture{t: t, bucket: b, data: &recordingReader{VersionedStore: b}, files: casBucketStore{b: b, bucket: "data"}, dir: t.TempDir()} |
| + x.f = &movesFiles{Files: x.files, Data: x.data, Bucket: "data", HomeRegion: "ie", Region: "uk", Box: "uk-box", |
| + FilesDir: func(host string) (string, string) { |
| + return filepath.Join(x.dir, host), filepath.Join(x.dir, host+".env") |
| + }} |
| + return x |
| +} |
| + |
| +func (x *filesFixture) envPath() string { return filepath.Join(x.dir, filesHost+".env") } |
| + |
| +func (x *filesFixture) writeEnv(body string) { |
| + x.t.Helper() |
| + if err := os.WriteFile(x.envPath(), []byte(body), 0o600); err != nil { |
| + x.t.Fatal(err) |
| + } |
| +} |
| + |
| +// publish writes a manifest revision for generation gen through the |
| +// ordinary publisher (a predecessor's session output) and returns it. |
| +func (x *filesFixture) publish(gen string, stage func(p *hostfiles.Publisher)) hostfiles.Manifest { |
| + x.t.Helper() |
| + p, err := hostfiles.Open(context.Background(), x.files, movesFilesPrefix(filesLoc, gen)) |
| + if err != nil { |
| + x.t.Fatal(err) |
| + } |
| + stage(p) |
| + if _, err := p.Commit(context.Background()); err != nil { |
| + x.t.Fatal(err) |
| + } |
| + m, found, err := hostfiles.Load(context.Background(), x.files, movesFilesPrefix(filesLoc, gen)) |
| + if err != nil || !found { |
| + x.t.Fatal(err, found) |
| + } |
| + return m |
| +} |
| + |
| +func (x *filesFixture) manifest(gen string) (hostfiles.Manifest, bool) { |
| + x.t.Helper() |
| + m, found, err := hostfiles.Load(context.Background(), x.files, movesFilesPrefix(filesLoc, gen)) |
| + if err != nil { |
| + x.t.Fatal(err) |
| + } |
| + return m, found |
| +} |
| + |
| +// serving is a serving claim on uk-box whose holder is at gen with the |
| +// cold admission chain given, newest last: each entry is |
| +// "<generation>:<region>:<predecessor>:<predecessor region>". |
| +func servingObserved(gen string, chain ...string) hostclaim.Observed { |
| + h := &hostclaim.Holder{Family: "instance", Key: "k", Account: "bab", App: "jam", Incarnation: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", Location: filesLoc, Generation: gen, Session: 3, WriterBox: "uk-box", Stamp: 7} |
| + for _, c := range chain { |
| + p := strings.Split(c, ":") |
| + h.Admissions = append(h.Admissions, hostclaim.AdmissionRecord{Generation: p[0], Region: p[1], Predecessor: p[2], PredecessorRegion: p[3], Mode: "cold", Session: 3, Box: "uk-box"}) |
| + } |
| + return hostclaim.Observed{Host: filesHost, ETag: "e1", Claim: &hostclaim.Claim{V: 1, Host: filesHost, Gen: 9, Holder: h, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "uk-box"}}} |
| +} |
| + |
| +func launchInputs(newGen string) ClaimLaunchInputs { |
| + return ClaimLaunchInputs{Identity: hostwrite.Identity{Config: "sha256:c", Release: "sha256:r"}, Admission: hostclaim.Admission{Box: "uk-box", Mode: "cold", NewGeneration: newGen, Region: "uk"}} |
| +} |
| + |
| +func sha(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) } |
| + |
| +// The prepared manifest (spec §7.6, R19-1; claim rev 6.30 R18-1): one |
| +// create-only manifest at sequence 0 under files/g-<P>/ in the location, |
| +// every content object create-only at sequence 0 under the same prefix, |
| +// the env file and the wake state as platform items, and the binding |
| +// {location, P, op, stamp} returned for the op journal. A different build |
| +// of the same generation is refused and leaves the manifest as it was. |
| +func TestPreparedManifestIsCreateOnlySequenceZeroAndBound(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + dir := filepath.Join(x.dir, filesHost) |
| + for rel, body := range map[string]string{"a.txt": "A", "uploads/b.bin": "BB"} { |
| + if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, rel)), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(dir, rel), []byte(body), 0o640); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := os.Symlink("a.txt", filepath.Join(dir, "link")); err != nil { |
| + t.Fatal(err) |
| + } |
| + x.writeEnv("K=1\n") |
| + wake := hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{{Name: "tick", Kind: "cron", Spec: "* * * * *", NextAt: 5}}, NextDue: 5, Replication: "hot"} |
| + pm, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + prefix := movesFilesPrefix(filesLoc, "mv1") |
| + if pm.Key != hostfiles.ManifestKey(prefix) || pm.Op != "mv1" || pm.Stamp != 7 || len(pm.SHA256) != 64 { |
| + t.Fatalf("binding: %+v", pm) |
| + } |
| + raw, ok := x.bucket.Current("data", pm.Key) |
| + if !ok || sha(raw) != pm.SHA256 { |
| + t.Fatalf("the manifest at %s is not the journalled bytes: %v", pm.Key, ok) |
| + } |
| + m, found := x.manifest("mv1") |
| + if !found || m.Seq != 0 { |
| + t.Fatalf("manifest found %v seq %d", found, m.Seq) |
| + } |
| + if err := hostfiles.ValidateManifest(m, []string{prefix}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(m.Entries) != 2 || m.Entries["a.txt"].Mode != 0o640 || m.Entries["uploads/b.bin"].SHA256 != sha([]byte("BB")) { |
| + t.Fatalf("entries: %+v", m.Entries) |
| + } |
| + for _, e := range m.Entries { |
| + if e.Seq != 0 || !strings.HasPrefix(e.Object, prefix+"objects/0000000000000000-") { |
| + t.Fatalf("entry object: %+v", e) |
| + } |
| + } |
| + if e := m.Platform[hostfiles.EnvItem]; e.SHA256 != sha([]byte("K=1\n")) || e.Seq != 0 { |
| + t.Fatalf("env item: %+v", e) |
| + } |
| + ws, found, err := hostfiles.LoadWake(ctx, x.files, prefix, m, nil) |
| + if err != nil || !found || len(ws.Timers) != 1 || ws.NextDue != 5 || ws.Replication != "hot" { |
| + t.Fatalf("wake: %+v %v %v", ws, found, err) |
| + } |
| + // A different build of the same generation: refused, the manifest kept. |
| + if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("changed"), 0o640); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake); !errors.Is(err, hostfiles.ErrManifestExists) { |
| + t.Fatalf("second build: %v", err) |
| + } |
| + if again, _ := x.bucket.Current("data", pm.Key); sha(again) != pm.SHA256 { |
| + t.Fatal("the prepared manifest was replaced") |
| + } |
| +} |
| + |
| +// A replay of the build (a crash after the objects, before or after the |
| +// manifest) reuses every object it finds by digest and the manifest by |
| +// key: the same binding, one version of each object, no new object. |
| +func TestPreparedManifestReplayReusesObjects(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + dir := filepath.Join(x.dir, filesHost) |
| + if err := os.MkdirAll(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("A"), 0o640); err != nil { |
| + t.Fatal(err) |
| + } |
| + x.writeEnv("K=1\n") |
| + wake := hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{}} |
| + first, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + second, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), wake) |
| + if err != nil { |
| + t.Fatalf("replay: %v", err) |
| + } |
| + if first != second { |
| + t.Fatalf("replay binding differs: %+v %+v", first, second) |
| + } |
| + vs, err := x.bucket.ListVersions(ctx, "data", movesFilesPrefix(filesLoc, "mv1")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(vs) != 4 { // three objects (a.txt, env, wake) and the manifest, one version each |
| + t.Fatalf("%d versions under the prepared prefix after a replay: %+v", len(vs), vs) |
| + } |
| +} |
| + |
| +// A cold admission establishes its generation's files manifest from the |
| +// newest valid manifest of its admitted predecessor chain, read by the |
| +// exact version the listing names: here g2 (the predecessor) never |
| +// published a manifest, so g1's current revision is the baseline. The copy |
| +// keeps every object origin-qualified under g1, continues the sequence, |
| +// and is create-only under g3. |
| +func TestEstablishCopiesTheNewestValidManifestByExactVersion(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + x.writeEnv("K=1\n") |
| + x.publish("g1", func(p *hostfiles.Publisher) { |
| + if err := p.Stage(ctx, "old.txt", []byte("old"), 0o600, false); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + g1 := x.publish("g1", func(p *hostfiles.Publisher) { |
| + if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := p.StagePlatform(ctx, hostfiles.EnvItem, []byte("K=1\n")); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + if g1.Seq != 3 { |
| + t.Fatalf("g1 at seq %d", g1.Seq) |
| + } |
| + o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk") |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil { |
| + t.Fatal(err) |
| + } |
| + if x.workRan != 1 { |
| + t.Fatalf("work ran %d times", x.workRan) |
| + } |
| + m, found := x.manifest("g3") |
| + if !found { |
| + t.Fatal("no manifest under g3") |
| + } |
| + if m.Seq != 3 || len(m.Entries) != 2 || m.Entries["a.txt"].Object != g1.Entries["a.txt"].Object || m.Entries["old.txt"].Object != g1.Entries["old.txt"].Object { |
| + t.Fatalf("copied manifest: %+v", m) |
| + } |
| + if e := m.Platform[hostfiles.EnvItem]; e.Object != g1.Platform[hostfiles.EnvItem].Object { |
| + t.Fatalf("the unchanged env file took a new object: %+v", e) |
| + } |
| + want := hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g1")) + "@" + x.bucket.LatestID("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g1"))) |
| + found = false |
| + for _, r := range x.data.reads { |
| + found = found || r == want |
| + } |
| + if !found { |
| + t.Fatalf("g1's manifest was not read at its exact latest version %s: %v", want, x.data.reads) |
| + } |
| + if _, has := x.manifest("g2"); has { |
| + t.Fatal("g2 gained a manifest") |
| + } |
| + // A repeat (a retry before the app executed) reads its own bytes back. |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil { |
| + t.Fatalf("repeat: %v", err) |
| + } |
| + if again, _ := x.manifest("g3"); again.Seq != 3 { |
| + t.Fatalf("the repeat replaced the manifest: seq %d", again.Seq) |
| + } |
| +} |
| + |
| +// The instance key rule (O1'): the manifest Establish leaves carries the |
| +// env file with exactly the bytes on disk when it runs, staged and |
| +// committed before it returns, and no adoption item; the local adoption |
| +// record is removed under the start's work. Establish never writes the |
| +// env file. A work that refuses leaves everything as it was. |
| +func TestEstablishCarriesTheEnvBytesAndDropsTheAdoptionItem(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + x.publish("g1", func(p *hostfiles.Publisher) { |
| + if err := p.StagePlatform(ctx, hostfiles.EnvItem, []byte("K=old\n")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := p.StagePlatform(ctx, instancekey.AdoptionItem, []byte(`{"v":2}`)); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + x.writeEnv("K=new\nCARLOS_INSTANCE_KEY=abc\n") |
| + adoption := filepath.Join(x.dir, filesHost+instancekey.AdoptionSuffix) |
| + if err := os.WriteFile(adoption, []byte(`{"v":2}`), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + o := servingObserved("g1", "g1:uk:@root:uk") |
| + x.workErr = hostbox.ErrChanged |
| + if err := x.f.Establish(ctx, o, launchInputs("g2"), x); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatalf("refused work: %v", err) |
| + } |
| + if _, found := x.manifest("g2"); found { |
| + t.Fatal("a refused work wrote the manifest") |
| + } |
| + if _, err := os.Stat(adoption); err != nil { |
| + t.Fatal("a refused work removed the adoption record") |
| + } |
| + x.workErr = nil |
| + if err := x.f.Establish(ctx, o, launchInputs("g2"), x); err != nil { |
| + t.Fatal(err) |
| + } |
| + m, found := x.manifest("g2") |
| + if !found { |
| + t.Fatal("no manifest under g2") |
| + } |
| + e := m.Platform[hostfiles.EnvItem] |
| + if e.SHA256 != sha([]byte("K=new\nCARLOS_INSTANCE_KEY=abc\n")) || !strings.HasPrefix(e.Object, movesFilesPrefix(filesLoc, "g2")+"objects/") { |
| + t.Fatalf("env item: %+v", e) |
| + } |
| + got, err := hostfiles.ReadPlatform(ctx, x.files, movesFilesPrefix(filesLoc, "g2"), m, hostfiles.EnvItem, nil) |
| + if err != nil || string(got) != "K=new\nCARLOS_INSTANCE_KEY=abc\n" { |
| + t.Fatalf("env bytes: %q %v", got, err) |
| + } |
| + if _, has := m.Platform[instancekey.AdoptionItem]; has { |
| + t.Fatal("the adoption item was carried into the new generation") |
| + } |
| + if _, err := os.Stat(adoption); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("the local adoption record remains: %v", err) |
| + } |
| + if disk, _ := os.ReadFile(x.envPath()); string(disk) != "K=new\nCARLOS_INSTANCE_KEY=abc\n" { |
| + t.Fatalf("Establish wrote the env file: %q", disk) |
| + } |
| + // Without an env file on disk there is nothing to carry: an error, and |
| + // no manifest. |
| + if err := os.Remove(x.envPath()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := x.f.Establish(ctx, servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk"), launchInputs("g3"), x); err == nil { |
| + t.Fatal("established without an env file") |
| + } |
| + if _, found := x.manifest("g3"); found { |
| + t.Fatal("a manifest was written without the env file") |
| + } |
| +} |
| + |
| +// Missing or malformed data is an error, never an empty-files bootstrap |
| +// (claim §6.4a): a chain with no manifest anywhere, with no pin evidence |
| +// or with evidence of published files, refuses; a malformed newest |
| +// manifest refuses rather than falling back to an older one; a chain that |
| +// leaves this region is not followed into another region's bucket. Nothing |
| +// is written in any of these. |
| +func TestEstablishNeverBootstrapsEmpty(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + x.writeEnv("K=1\n") |
| + o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk") |
| + // No manifest anywhere, no evidence. |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrNoEvidence) { |
| + t.Fatalf("no manifest, no pin: %v", err) |
| + } |
| + // A pin that evidences published files is not evidence of none. |
| + x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) { |
| + return hostfiles.PinnedFiles{SHA256: "ab", Retained: "retained/ab"}, true, nil |
| + } |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrNoEvidence) { |
| + t.Fatalf("no manifest, published pin: %v", err) |
| + } |
| + // A pin reader that fails is an error, not absence. |
| + x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) { |
| + return hostfiles.PinnedFiles{}, false, errors.New("pin unreadable") |
| + } |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err == nil || errors.Is(err, hostfiles.ErrNoEvidence) { |
| + t.Fatalf("unreadable pin: %v", err) |
| + } |
| + x.f.Evidence = nil |
| + // The newest manifest is malformed: refused, not skipped. |
| + x.publish("g1", func(p *hostfiles.Publisher) { |
| + if err := p.Stage(ctx, "a.txt", []byte("A"), 0o600, false); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| + x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "g2")), []byte(`{"v":2,"seq":1,"entries":{"x":{"object":"elsewhere/objects/0000000000000001-`+sha(nil)+`","sha256":"`+sha(nil)+`","seq":1}},"platform":{}}`)) |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); !errors.Is(err, hostfiles.ErrOrigin) { |
| + t.Fatalf("malformed newest manifest: %v", err) |
| + } |
| + // The predecessor lives in another region: its manifest is not in this |
| + // bucket and the chain is not followed there. |
| + far := servingObserved("g5", "g5:uk:g1:ie") |
| + if err := x.f.Establish(ctx, far, launchInputs("g6"), x); !errors.Is(err, hostfiles.ErrNoEvidence) { |
| + t.Fatalf("predecessor in another region: %v", err) |
| + } |
| + for _, gen := range []string{"g3", "g6"} { |
| + if _, found := x.manifest(gen); found { |
| + t.Fatalf("a manifest was written under %s", gen) |
| + } |
| + } |
| + if x.workRan != 0 { |
| + t.Fatalf("work ran %d times with nothing to establish", x.workRan) |
| + } |
| +} |
| + |
| +// The one empty start besides the claim's own bootstrap (in.Empty after |
| +// EmptyAuthority): the pin evidences a host that never published files |
| +// (region HA stage B's rule, hostfiles.PinnedFiles). Then the generation |
| +// starts from no manifest, carrying only the env file. The claim's empty |
| +// bootstrap reads no chain at all. |
| +func TestEstablishHostThatNeverPublishedFiles(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + x.writeEnv("K=1\n") |
| + x.f.Evidence = func(context.Context, hostclaim.Observed) (hostfiles.PinnedFiles, bool, error) { |
| + return hostfiles.PinnedFiles{}, true, nil |
| + } |
| + o := servingObserved("g2", "g1:uk:@root:uk", "g2:uk:g1:uk") |
| + if err := x.f.Establish(ctx, o, launchInputs("g3"), x); err != nil { |
| + t.Fatal(err) |
| + } |
| + m, found := x.manifest("g3") |
| + if !found || m.Seq != 1 || len(m.Entries) != 0 || len(m.Platform) != 1 || m.Platform[hostfiles.EnvItem].SHA256 != sha([]byte("K=1\n")) { |
| + t.Fatalf("never-published bootstrap: %+v %v", m, found) |
| + } |
| + // The claim's own empty bootstrap: a created incarnation before its |
| + // first writer, admitted after EmptyAuthority. |
| + x.f.Evidence = nil |
| + empty := servingObserved("") |
| + empty.Claim.Holder.Session, empty.Claim.Holder.WriterBox = 0, "" |
| + in := launchInputs("g1") |
| + in.Empty = true |
| + if err := x.f.Establish(ctx, empty, in, x); err != nil { |
| + t.Fatal(err) |
| + } |
| + if m, found := x.manifest("g1"); !found || m.Seq != 1 || len(m.Entries) != 0 || len(x.data.reads) != 0 { |
| + t.Fatalf("empty bootstrap: %+v %v reads %v", m, found, x.data.reads) |
| + } |
| +} |
| + |
| +// The probe's admission opens the prepared generation itself (in.Prepared, |
| +// NewGeneration = P): its baseline is P's own prepared manifest at |
| +// sequence 0, which must name destination-local objects only; the env file |
| +// on disk (now carrying the minted key) is staged over it at sequence 1, |
| +// a new version of the same key, and the sequence-0 version stays |
| +// readable by exact version. A prepared manifest naming another |
| +// generation's objects refuses. |
| +func TestEstablishOnThePreparedGenerationContinuesFromSequenceZero(t *testing.T) { |
| + x := newFilesFixture(t) |
| + ctx := context.Background() |
| + dir := filepath.Join(x.dir, filesHost) |
| + if err := os.MkdirAll(dir, 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("A"), 0o640); err != nil { |
| + t.Fatal(err) |
| + } |
| + x.writeEnv("K=1\n") |
| + pm, err := x.f.BuildPrepared(ctx, filesLoc, "mv1", "mv1", 7, dir, x.envPath(), hostfiles.WakeState{V: 1, Host: filesHost, Timers: []hostfiles.WakeTimer{}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + seq0 := x.bucket.LatestID("data", pm.Key) |
| + x.writeEnv("K=1\nCARLOS_INSTANCE_KEY=abc\n") |
| + o := servingObserved("cur", "cur:ie:@root:ie") |
| + in := launchInputs("mv1") |
| + in.Prepared = true |
| + if err := x.f.Establish(ctx, o, in, x); err != nil { |
| + t.Fatal(err) |
| + } |
| + m, found := x.manifest("mv1") |
| + if !found || m.Seq != 1 || m.Entries["a.txt"].Seq != 0 || m.Platform[hostfiles.EnvItem].SHA256 != sha([]byte("K=1\nCARLOS_INSTANCE_KEY=abc\n")) { |
| + t.Fatalf("after the probe's establishment: %+v %v", m, found) |
| + } |
| + raw, err := x.bucket.GetVersion(ctx, "data", pm.Key, seq0) |
| + if err != nil || sha(raw) != pm.SHA256 { |
| + t.Fatalf("the sequence-0 version is gone: %v", err) |
| + } |
| + // A prepared manifest that names a foreign object (P6-D3) refuses. |
| + foreign := hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{"x": {Object: movesFilesPrefix(filesLoc, "cur") + "objects/0000000000000000-" + sha(nil), SHA256: sha(nil), Seq: 0}}, Platform: map[string]hostfiles.Entry{}} |
| + body, _ := json.Marshal(foreign) |
| + x.bucket.Put("data", hostfiles.ManifestKey(movesFilesPrefix(filesLoc, "mv2")), body) |
| + in = launchInputs("mv2") |
| + in.Prepared = true |
| + if err := x.f.Establish(ctx, o, in, x); !errors.Is(err, hostfiles.ErrOrigin) { |
| + t.Fatalf("foreign prepared manifest: %v", err) |
| + } |
| + // No prepared manifest at P: an error, never a fresh start. |
| + in = launchInputs("mv3") |
| + in.Prepared = true |
| + if err := x.f.Establish(ctx, o, in, x); err == nil || errors.Is(err, hostfiles.ErrNoEvidence) { |
| + t.Fatalf("missing prepared manifest: %v", err) |
| + } |
| + if _, found := x.manifest("mv3"); found { |
| + t.Fatal("a manifest was written for a missing prepared generation") |
| + } |
| +} |