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 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "io/fs"
13 "os"
14 "path/filepath"
15 "reflect"
16 "sort"
17 "strconv"
18 "strings"
19
20 "github.com/carlosframework/platform/internal/hostbox"
21 "github.com/carlosframework/platform/internal/hostclaim"
22 "github.com/carlosframework/platform/internal/hostfiles"
23 "github.com/carlosframework/platform/internal/hostwrite"
24 "github.com/carlosframework/platform/internal/instancefs"
25 "github.com/carlosframework/platform/internal/instancekey"
26 "github.com/carlosframework/platform/internal/lease"
27 "github.com/carlosframework/platform/internal/moves"
28 "github.com/carlosframework/platform/internal/regionname"
29 "github.com/carlosframework/platform/internal/release"
30)
31
32// movesFiles is the C8 files-manifest provider (claim §6.2a P6-D3, moves
33// spec §7.6, index C8; Q3 ruling). It is two pieces of region HA's files
34// publisher API, on hostfiles:
35//
36// - BuildPrepared, the prepared-manifest build an installation runs in
37// `local` before any session exists: create-only content objects plus
38// one create-only sequence-0 manifest under files/g-<P>/ in the
39// location, bound to {location, P, op, stamp}, replayed by digest and
40// key (hostfiles.Prepared).
41// - Establish, the reader a cold admission establishes its files manifest
42// with: the newest valid manifest of the admitted predecessor chain,
43// read by exact version, copied create-only into the admitted
44// generation with the env file's bytes on disk staged and the adoption
45// item dropped (hostfiles.NewFromCopy; instance key O1').
46//
47// The session-bound publisher (sequence 1 onwards under a session) stays
48// region HA's; moves does not need it, the cut archive is the transfer.
49//
50// Missing or malformed data is an error, never an empty-files bootstrap
51// (claim §6.4a). The two empty starts are the claim's own bootstrap
52// (in.Empty, proved by EmptyAuthority before Establish runs) and a pin
53// that evidences a host that never published files (region HA stage B's
54// rule, hostfiles.PinnedFiles.NeverPublished, read through Evidence).
55// Absence of a manifest, of a pin, or of a reader is never evidence: with
56// Evidence nil, a chain without a manifest refuses every wake.
57type movesFiles struct {
58 emptyRecovery claimEmptyRecovery
59 Seed *filesSeedOperation
60 // Files is this box's data bucket as a release.Store: current reads
61 // with the object's etag, create-only and compare-and-swap writes
62 // (the S3 store's Put with If-None-Match and If-Match). Every write
63 // this provider makes goes through it.
64 Files release.Store
65 // Data is the same bucket as a versioned store (the claim's
66 // hostbox.RequestStore in production): the chain's manifests and the
67 // prepared manifest are read at the exact version the listing or the
68 // op journal names, never by key, and BuildPrepared writes through it
69 // create-only.
70 Data lease.VersionedStore
71 // DataDir is the box's data directory, where the op journals live
72 // (moves.ReadJournal): the probe's admission reads the prepared
73 // manifest's binding from the op journal local wrote.
74 DataDir string
75 // Bucket is the data bucket's name, HomeRegion the fleet's signed home
76 // region, Region this box's engine region (park.PlacementRegion of its
77 // placement) and Box its id. Every region comparison goes through
78 // regionname.SameRegion with HomeRegion as the alias.
79 Bucket, HomeRegion, Region, Box string
80 // EnvFile resolves the host's env file on this box for the wake's
81 // database path, as the launch source that minted the instance key into
82 // it resolved it (claimEnvFileFor): the nested instance directory's for
83 // a nested row, the flat one for a flat row, and an error for anything
84 // it cannot place.
85 EnvFile func(host, dbPath string) (claimEnvFile, error)
86 // Proof proves origins against admissions the claim has recorded
87 // (platform-1e's ruling, fix round 3; claim Plan 15): in production
88 // ClaimOriginProof over the deployment store and the verified home
89 // alias, one batch call per establishment. nil refuses: there is no
90 // other route, and retained admissions are never evidence.
91 Proof movesOriginProof
92 // Evidence reads the claim pin's files evidence for a host, when the
93 // claim carries a pin: region HA's ReadPin over the op journal's pin
94 // reference. false means the claim carries no pin. nil means no pin
95 // reader is wired on this box, which reads as no evidence.
96 Evidence func(ctx context.Context, o hostclaim.Observed) (hostfiles.PinnedFiles, bool, error)
97}
98
99var _ ClaimFilesManifest = (*movesFiles)(nil)
100
101// preparedManifest is the files_manifest journal step's value: the
102// binding {location, P, op, stamp} of the prepared manifest an install
103// wrote, with the digest and the exact version of its bytes. Key is
104// <location>/files/g-<P>/manifest.json, which names the location and P.
105// The probe's admission takes exactly Version and checks it against
106// SHA256 (fix round 1, finding 2).
107type preparedManifest struct {
108 Key string `json:"key"`
109 SHA256 string `json:"sha256"`
110 Version string `json:"version"`
111 Op string `json:"op"`
112 Stamp uint64 `json:"stamp"`
113}
114
115// decodePreparedManifest decodes a journalled binding and holds it to the
116// expected key, op and stamp (fix round 1, finding 3). An empty value,
117// malformed JSON, a binding for another key, op or stamp, or one without a
118// digest or a version, is an error: the journal is damaged or another
119// install's, and the step is neither done nor rebuilt over it.
120func decodePreparedManifest(raw, key, op string, stamp uint64) (preparedManifest, error) {
121 var pm preparedManifest
122 if raw == "" {
123 return pm, fmt.Errorf("%w: the journal's %s step is empty", errLandingChanged, landStepFilesManifest)
124 }
125 if err := json.Unmarshal([]byte(raw), &pm); err != nil {
126 return pm, fmt.Errorf("%w: the journal's %s step %q: %v", errLandingChanged, landStepFilesManifest, raw, err)
127 }
128 if pm.Key != key || pm.Op != op || pm.Stamp != stamp || !claimHexDigest(pm.SHA256) || pm.Version == "" || pm.Version == "null" {
129 return pm, fmt.Errorf("%w: the journal's %s step %+v is not the binding of %s by %s at stamp %d", errLandingChanged, landStepFilesManifest, pm, key, op, stamp)
130 }
131 return pm, nil
132}
133
134// movesFilesPrefix is a generation's files prefix in its location,
135// <location>/files/g-<generation>/, the rule pinFilesLiveSet and the park
136// view's files live set share (spec §7.6), and region HA's relocation
137// with them: moves.GenerationFilesPrefix, the one rule.
138func movesFilesPrefix(location, generation string) string {
139 return moves.GenerationFilesPrefix(location, generation)
140}
141
142// movesFilesGeneration accepts a generation name as the claim does
143// (claimFreshGeneration's shape), never the origin or a path.
144func movesFilesGeneration(g string) bool {
145 return g != "" && g != "." && g != ".." && g != hostclaim.RootGeneration && len(g) <= 64 && !strings.ContainsAny(g, "/\\\x00\r\n")
146}
147
148// createOnlyStore is a release.Store over a versioned store for one
149// bucket: current reads by the listing's latest version, create-only
150// writes (PutIfAbsent), never a replace. BuildPrepared writes through it,
151// so an install needs only the box's versioned store.
152type createOnlyStore struct {
153 data lease.VersionedStore
154 bucket string
155}
156
157func (s createOnlyStore) Get(ctx context.Context, key string) ([]byte, string, error) {
158 vs, err := s.data.ListVersions(ctx, s.bucket, key)
159 if err != nil {
160 return nil, "", err
161 }
162 for _, v := range vs {
163 if v.Key == key && v.IsLatest {
164 if v.DeleteMarker || v.VersionID == "" || v.VersionID == "null" {
165 return nil, "", fmt.Errorf("host: %s is not a readable version: %w", key, os.ErrNotExist)
166 }
167 body, err := s.data.GetVersion(ctx, s.bucket, key, v.VersionID)
168 return body, v.VersionID, err
169 }
170 }
171 return nil, "", fmt.Errorf("host: %s: %w", key, os.ErrNotExist)
172}
173
174func (s createOnlyStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
175 if ifMatch != "" {
176 return "", errors.New("host: the prepared manifest build never replaces an object")
177 }
178 id, err := s.data.PutIfAbsent(ctx, s.bucket, key, body)
179 if errors.Is(err, lease.ErrExists) {
180 return "", fmt.Errorf("%w: %s", release.ErrCASConflict, key)
181 }
182 return id, err
183}
184
185func (createOnlyStore) List(context.Context, string) ([]string, error) {
186 return nil, errors.New("host: the prepared manifest build does not list")
187}
188func (createOnlyStore) ListDirs(context.Context, string) ([]string, error) {
189 return nil, errors.New("host: the prepared manifest build does not list")
190}
191func (createOnlyStore) Delete(context.Context, string) error {
192 return errors.New("host: the prepared manifest build never deletes")
193}
194
195// buildPreparedManifest is BuildPrepared over a versioned store: the
196// install's own (ir.versioned), which needs no compare-and-swap.
197func buildPreparedManifest(ctx context.Context, data lease.VersionedStore, bucket, location, generation, op string, stamp uint64, dir, envPath string, wantEnv bool, wake hostfiles.WakeState) (preparedManifest, error) {
198 if data == nil || bucket == "" {
199 return preparedManifest{}, hostwrite.ErrUnavailable
200 }
201 return buildPrepared(ctx, createOnlyStore{data: data, bucket: bucket}, location, generation, op, stamp, dir, envPath, wantEnv, wake)
202}
203
204// BuildPrepared writes the prepared manifest of generation (the
205// installation's prepared destination generation P, g-<move id> or
206// g-<rollback id>, never holder.generation: R19-1) under location: every
207// regular file under dir as a create-only object at sequence 0 (a file
208// over hostfiles.MaxFileBytes is recorded as skipped; anything not a
209// regular file is left out, as SyncDir leaves it), the env file at envPath
210// as the EnvItem, the wake state as the WakeItem, then the one create-only
211// sequence-0 manifest. It returns the binding for the op journal: the key,
212// the manifest's digest and exact version, op and stamp. A replay reuses
213// every object by digest and reads the manifest back by key; a different
214// manifest at the key is hostfiles.ErrManifestExists.
215//
216// Missing inputs are never an empty image (fix round 1, finding 4): dir
217// must exist (the install placed it), and the env file must exist exactly
218// when wantEnv says the install placed one (the archive's evidence, the
219// install receipt's Env identity). A vanished tree, a vanished env file,
220// or an env file nobody placed, refuses before any write.
221//
222// The caller runs it as op.exec in a phase that permits installation,
223// after the install's own claim sync and stamp capture (local_).
224func (f *movesFiles) BuildPrepared(ctx context.Context, location, generation, op string, stamp uint64, dir, envPath string, wantEnv bool, wake hostfiles.WakeState) (preparedManifest, error) {
225 if f == nil {
226 return preparedManifest{}, hostwrite.ErrUnavailable
227 }
228 return buildPreparedManifest(ctx, f.Data, f.Bucket, location, generation, op, stamp, dir, envPath, wantEnv, wake)
229}
230
231func buildPrepared(ctx context.Context, s release.Store, location, generation, op string, stamp uint64, dir, envPath string, wantEnv bool, wake hostfiles.WakeState) (preparedManifest, error) {
232 if ctx == nil || location == "" || !movesFilesGeneration(generation) || op == "" || stamp == 0 || dir == "" || envPath == "" {
233 return preparedManifest{}, fmt.Errorf("%w: a prepared manifest names its location, generation, op, stamp, files directory and env path", hostwrite.ErrUnavailable)
234 }
235 // The inputs first, before any write: the placed tree and the env file
236 // as the install's evidence says.
237 if st, err := os.Lstat(dir); err != nil || !st.IsDir() {
238 return preparedManifest{}, fmt.Errorf("%w: the files directory %s the install placed is not there (%v)", errLandingChanged, dir, err)
239 }
240 env, err := os.ReadFile(envPath)
241 switch {
242 case err == nil && !wantEnv:
243 return preparedManifest{}, fmt.Errorf("%w: an env file is at %s, which the install did not place", errLandingChanged, envPath)
244 case errors.Is(err, fs.ErrNotExist) && wantEnv:
245 return preparedManifest{}, fmt.Errorf("%w: the env file %s the install placed is not there", errLandingChanged, envPath)
246 case err != nil && !errors.Is(err, fs.ErrNotExist):
247 return preparedManifest{}, err
248 }
249 prefix := movesFilesPrefix(location, generation)
250 p, err := hostfiles.NewPrepared(s, prefix)
251 if err != nil {
252 return preparedManifest{}, err
253 }
254 // The tree is read through an os.Root over dir, every name relative to
255 // it (#236 fix round 1): a path within the files tree's cap (4095 bytes,
256 // which the archive the install extracted holds every path to) is past
257 // PATH_MAX once the data directory is prefixed, so an absolute walk
258 // fails with ENAMETOOLONG after the cut. The root also keeps every read
259 // inside dir.
260 //
261 // Census ruling moves-overcap-cut-236 (platform-1e, 2026-10-15;
262 // binding): no read here falls back to a path-based open. A root that
263 // cannot be opened, or a name it cannot open (readFileBounded), refuses
264 // the build; nothing retries by absolute path.
265 root, err := os.OpenRoot(dir)
266 if err != nil {
267 return preparedManifest{}, fmt.Errorf("%w: the files directory %s the install placed: %v", errLandingChanged, dir, err)
268 }
269 defer root.Close()
270 err = fs.WalkDir(root.FS(), ".", func(rel string, d fs.DirEntry, err error) error {
271 if err != nil {
272 return err
273 }
274 if rel == "." || d.IsDir() || !d.Type().IsRegular() {
275 return nil
276 }
277 info, err := d.Info()
278 if err != nil {
279 return err
280 }
281 // The size before any read, and the read bounded at the limit
282 // plus one byte (fix round 1, finding 5): a file over the limit, by
283 // its stat or by growing while it is read, is recorded as skipped
284 // and never held whole in memory.
285 if info.Size() > hostfiles.MaxFileBytes {
286 return p.StageSkipped(rel, movesFilesTooLarge(info.Size()))
287 }
288 body, tooLarge, err := readFileBounded(root, rel, hostfiles.MaxFileBytes)
289 if err != nil {
290 return err
291 }
292 if tooLarge {
293 return p.StageSkipped(rel, movesFilesTooLarge(int64(len(body))))
294 }
295 return p.Stage(ctx, rel, body, uint32(info.Mode().Perm()))
296 })
297 if err != nil {
298 return preparedManifest{}, fmt.Errorf("prepared manifest of %s: %w", dir, err)
299 }
300 if wantEnv {
301 if err := p.StagePlatform(ctx, hostfiles.EnvItem, env); err != nil {
302 return preparedManifest{}, err
303 }
304 }
305 body, err := json.Marshal(wake)
306 if err != nil {
307 return preparedManifest{}, fmt.Errorf("marshal wake state: %w", err)
308 }
309 if err := p.StagePlatform(ctx, hostfiles.WakeItem, body); err != nil {
310 return preparedManifest{}, err
311 }
312 _, raw, version, err := p.Commit(ctx)
313 if err != nil {
314 return preparedManifest{}, err
315 }
316 if version == "" || version == "null" {
317 return preparedManifest{}, fmt.Errorf("%w: the store answered no version for %s", hostwrite.ErrUnavailable, hostfiles.ManifestKey(prefix))
318 }
319 sum := sha256.Sum256(raw)
320 return preparedManifest{Key: hostfiles.ManifestKey(prefix), SHA256: hex.EncodeToString(sum[:]), Version: version, Op: op, Stamp: stamp}, nil
321}
322
323// movesFilesTooLarge is a skipped entry's reason for a file over the
324// limit; size is what the stat or the bounded read saw.
325func movesFilesTooLarge(size int64) string {
326 return fmt.Sprintf("over the %d-byte limit (%d bytes or more)", hostfiles.MaxFileBytes, size)
327}
328
329// readFileBounded reads rel, relative to root, whole when it holds at most
330// limit bytes. It reads at most limit+1 bytes however large the file is or
331// becomes while it is read; tooLarge reports that the file exceeded the
332// limit, and the bytes returned with it are then not the whole file. A
333// test seam.
334var readFileBounded = func(root *os.Root, rel string, limit int64) ([]byte, bool, error) {
335 // Through the root only (census ruling moves-overcap-cut-236, binding):
336 // an open that fails is the error, never a retry by path.
337 f, err := root.Open(rel)
338 if err != nil {
339 return nil, false, err
340 }
341 defer f.Close()
342 body, err := io.ReadAll(io.LimitReader(f, limit+1))
343 if err != nil {
344 return nil, false, err
345 }
346 return body, int64(len(body)) > limit, nil
347}
348
349// movesFilesChain is the admitted predecessor chain whose manifests are in
350// this region's bucket, newest first: the holder's generation, then each
351// cold admission's predecessor while it is a named generation in this
352// region (regionname.SameRegion against the home alias). A predecessor in
353// another region ends the walk: its objects are not here, and a target's
354// manifest never references a source-region object (P6-D3). Warm
355// admissions keep their generation and are passed over.
356func movesFilesChain(h *hostclaim.Holder, home, region string) []string {
357 if h == nil {
358 return nil
359 }
360 return movesFilesChainFrom(h, h.Generation, home, region)
361}
362
363// recoverySourceGeneration is the generation a recovery's files baseline
364// starts from (whole-branch fixes, item 3): for a restoring or reverting
365// op, the source's generation the claim journalled at the flip (src_gen),
366// which the recovery's database restore is pinned to as well; "" names a
367// legacy source with no files generation. ok is false for any other op.
368func recoverySourceGeneration(c *hostclaim.Claim) (string, bool) {
369 if c == nil {
370 return "", false
371 }
372 switch c.Op.Kind {
373 case hostclaim.KindRestoring, hostclaim.KindReverting:
374 return c.Op.Journal[hostclaim.JournalSrcGen], true
375 }
376 return "", false
377}
378
379// restoresSourceBaseline reports whether a recovery admission restores the
380// recorded source baseline (platform-1e's framing of item 3): a restoring
381// op that is not a dirty return (a dirty return journals its return seal
382// and restores the rollback generation or a certified descendant, never
383// the stopped source), with a journalled src_gen. The files bridge from
384// src_gen applies only to these, and only while their recovery lineage
385// holds no manifest (the database selector's "not rooted"); every other
386// admission follows its recovery lineage.
387func restoresSourceBaseline(c *hostclaim.Claim) bool {
388 if c == nil || c.Op.Kind != hostclaim.KindRestoring || c.Op.Journal[moves.JournalReturnSeal] != "" {
389 return false
390 }
391 src, ok := recoverySourceGeneration(c)
392 return ok && src != ""
393}
394
395// movesFilesChainFrom is movesFilesChain started at generation start
396// instead of the holder's: start, then each cold admission's predecessor
397// in this region, as the admissions record them.
398func movesFilesChainFrom(h *hostclaim.Holder, start, home, region string) []string {
399 if h == nil || !movesFilesGeneration(start) {
400 return nil
401 }
402 cur := start
403 chain := []string{cur}
404 seen := map[string]bool{cur: true}
405 for i := len(h.Admissions) - 1; i >= 0; i-- {
406 a := h.Admissions[i]
407 if a.Generation != cur || a.Mode != "cold" {
408 continue
409 }
410 next := a.Predecessor
411 if !movesFilesGeneration(next) || seen[next] || !regionname.SameRegion(home, a.PredecessorRegion, region) {
412 break
413 }
414 chain = append(chain, next)
415 seen[next] = true
416 cur = next
417 }
418 return chain
419}
420
421// manifestVersion is the current version of a manifest key as the listing
422// names it: "" when the key has no current object. A delete marker or an
423// unversioned current object is an error.
424func (f *movesFiles) manifestVersion(ctx context.Context, key string) (string, error) {
425 return movesManifestVersion(ctx, f.Data, f.Bucket, key)
426}
427
428// movesManifestVersion is manifestVersion over any versioned reader of the
429// data bucket: the cut's preflight reads with the moves worker's own
430// (sourceFilesRefusal), the establishment with the claim's.
431func movesManifestVersion(ctx context.Context, data lease.VersionedStore, bucket, key string) (string, error) {
432 vs, err := data.ListVersions(ctx, bucket, key)
433 if err != nil {
434 return "", err
435 }
436 for _, v := range vs {
437 if v.Key != key || !v.IsLatest {
438 continue
439 }
440 if v.DeleteMarker || v.VersionID == "" || v.VersionID == "null" {
441 return "", fmt.Errorf("host: the files manifest %s is not a readable version", key)
442 }
443 return v.VersionID, nil
444 }
445 return "", nil
446}
447
448// newestManifest is the newest valid manifest of the chain, read by exact
449// version, with the origins its entries name (movesFilesOrigins: the
450// location's rule, not the chain's). A generation without a manifest is
451// passed over; the first manifest found is validated and is the baseline,
452// malformed or not (a malformed newest manifest refuses, never an older
453// one in its place). found is false when no generation of the chain has a
454// manifest.
455func (f *movesFiles) newestManifest(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, location string, chain []string) (hostfiles.Manifest, []string, bool, error) {
456 m, key, version, found, err := movesFilesSelect(ctx, f.Data, f.Bucket, location, chain)
457 if err != nil || !found {
458 return hostfiles.Manifest{}, nil, false, err
459 }
460 allowed, err := f.movesFilesAllowed(ctx, o, in, location, in.NewGeneration, m)
461 if err != nil {
462 return hostfiles.Manifest{}, nil, false, fmt.Errorf("host: the files manifest %s@%s: %w", key, version, err)
463 }
464 return m, allowed, true, nil
465}
466
467// movesFilesSelect is newestManifest's selection, apart from its
468// validation: the first generation of chain whose manifest key has a
469// current version, read at exactly that version, with the key and the
470// version. found is false when no generation of chain has a manifest. The
471// establishment (newestManifest, the src_gen bridge included) and the
472// cut's preflight (sourceFilesRefusal) both select through it, so the cut
473// checks the manifest a recovery would restore, not a guess at it (#236).
474func movesFilesSelect(ctx context.Context, data lease.VersionedStore, bucket, location string, chain []string) (m hostfiles.Manifest, key, version string, found bool, err error) {
475 for _, g := range chain {
476 prefix := movesFilesPrefix(location, g)
477 key := hostfiles.ManifestKey(prefix)
478 version, err := movesManifestVersion(ctx, data, bucket, key)
479 if err != nil {
480 return hostfiles.Manifest{}, "", "", false, err
481 }
482 if version == "" {
483 continue
484 }
485 m, found, err := hostfiles.Load(ctx, versionStore{data: data, bucket: bucket, key: key, version: version}, prefix)
486 if err != nil {
487 return hostfiles.Manifest{}, "", "", false, err
488 }
489 if !found {
490 return hostfiles.Manifest{}, "", "", false, fmt.Errorf("host: the files manifest %s@%s is not readable", key, version)
491 }
492 return m, key, version, true, nil
493 }
494 return hostfiles.Manifest{}, "", "", false, nil
495}
496
497// movesFilesRestorable is the reader's path cap as moves applies it
498// (hostfiles.RestorableWithinCap, the rule checkManifest applies to every
499// entry, live, tombstoned or skipped): a manifest that fails it is
500// hostfiles.ErrTooDeep, and no establishment restores it. The recovery's
501// establishment (movesFilesAllowed) and the cut's preflight
502// (sourceFilesRefusal) both call it, so a move whose recovery could not
503// restore the source's files never starts its cut (#236; Paul's ruling of
504// 2026-10-15). If region HA relaxes the readers' rule, RestorableWithinCap
505// follows it, and both sides here with it.
506func movesFilesRestorable(m hostfiles.Manifest) error {
507 return hostfiles.RestorableWithinCap(m)
508}
509
510// movesOriginProof proves that generations of the holder's incarnation
511// were admitted, by the claim's own records (platform-1e's ruling, fix
512// round 3; claim Plan 15): provenance comes from the claim, never from a
513// manifest, whose writer holds the bucket credentials that could write
514// its Origins too. The implementation is Plan 15's ClaimOriginProof: a
515// batch Proven over the whole wanted set, which authenticates the bound
516// observation against the stored claim (the CAS result the caller holds,
517// never a bare Admission.NewGeneration), walks the claim's recorded
518// admissions once from its committed head, stops as soon as every wanted
519// generation is proven, bounds each node read, and takes home aliases only
520// through the verified roster binding. The chain starts empty: nothing is
521// seeded from retained admissions, which are never evidence, and a claim
522// with no recorded head proves nothing, so every existing host refuses
523// until the #250 files backfill has run. That is intended. A false answer
524// is ErrOrigin; an error is the error.
525type movesOriginProof interface {
526 Proven(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, generations []string) (map[string]bool, error)
527}
528
529// proveOrigins is the one route to origin proof (fix round 4; Plan 15):
530// one Proven call over the deduplicated set of generations, and a
531// generation's files prefix under location enters the allowed set only
532// when that call proves it. A generation the proof does not prove is
533// ErrOrigin, naming the first; an error is the error; no proof wired
534// refuses. No per-origin calls (Plan 15's review: one walk per
535// establishment).
536func (f *movesFiles) proveOrigins(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, location string, generations []string) ([]string, error) {
537 if f.Proof == nil {
538 return nil, fmt.Errorf("%w: no origin proof is wired", hostwrite.ErrUnavailable)
539 }
540 set := make([]string, 0, len(generations))
541 seen := map[string]bool{}
542 for _, gen := range generations {
543 if !movesFilesGeneration(gen) {
544 return nil, fmt.Errorf("%w: %q is not a generation name", hostfiles.ErrOrigin, gen)
545 }
546 if !seen[gen] {
547 seen[gen] = true
548 set = append(set, gen)
549 }
550 }
551 proven, err := f.Proof.Proven(ctx, o, in, set)
552 if err != nil {
553 return nil, fmt.Errorf("prove the origins of %s: %w", location, err)
554 }
555 origins := make([]string, 0, len(set))
556 for _, gen := range set {
557 if !proven[gen] {
558 return nil, fmt.Errorf("%w: %s is not proven by the claim's recorded admissions", hostfiles.ErrOrigin, movesFilesPrefix(location, gen))
559 }
560 origins = append(origins, movesFilesPrefix(location, gen))
561 }
562 // The prefixes are sorted, not the generation names: the two do not
563 // order alike ("mv1" before "mv1-old", "g-mv1-old/" before "g-mv1/"),
564 // and this is the carried set SetOrigins stores and establishedOver
565 // compares (Astra's finding on the Plan 15 switch).
566 sort.Strings(origins)
567 return origins, nil
568}
569
570// movesFilesAllowed is the origin set an admission allows for m, the
571// manifest it establishes from (Task 6b, the moves side of claim Plan 15's
572// review): the origins m's entries actually reference, each proven against
573// the claim's records through proveOrigin after the location rule
574// (movesFilesOrigins), plus the one generation this admission opens
575// (admitted: proven through the same route, round 4's binding, one call).
576// Nothing else is proven and nothing else is carried: the retained chain's
577// other generations are not candidates, and a carried hint
578// (Manifest.Origins) that no entry references is neither proven nor
579// carried forward, so the proof's cost follows the live references, not
580// the history. A referenced origin the proof cannot prove is ErrOrigin.
581// Then ValidateManifest over the proven set, which is the new manifest's
582// carried set (SetOrigins). Nothing refused before is accepted now: every
583// referenced origin and the admitted generation go through the proof as
584// before; only unreferenced hints and unreferenced chain generations, which
585// no entry depends on, are no longer asked about.
586func (f *movesFiles) movesFilesAllowed(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, location, admitted string, m hostfiles.Manifest) ([]string, error) {
587 // The path cap first, by the rule the cut's preflight applies
588 // (movesFilesRestorable): ValidateManifest below refuses the same
589 // manifest with the same error, so this only refuses it before any
590 // origin proof, and keeps the two sides on one function.
591 if err := movesFilesRestorable(m); err != nil {
592 return nil, err
593 }
594 named, err := movesFilesOrigins(location, "", m)
595 if err != nil {
596 return nil, err
597 }
598 base := moves.FilesGenerationBase(location)
599 generations := []string{admitted}
600 for _, origin := range named {
601 rest, under := strings.CutPrefix(origin, base)
602 gen, slash := strings.CutSuffix(rest, "/")
603 if !under || !slash || !movesFilesGeneration(gen) {
604 return nil, fmt.Errorf("%w: %s is not a generation's files prefix of %s", hostfiles.ErrOrigin, origin, location)
605 }
606 generations = append(generations, gen)
607 }
608 // One Proven call over the deduplicated referenced origins plus the
609 // admitted generation (Plan 15).
610 allowed, err := f.proveOrigins(ctx, o, in, location, generations)
611 if err != nil {
612 return nil, err
613 }
614 if err := hostfiles.ValidateManifest(m, allowed); err != nil {
615 return nil, err
616 }
617 return allowed, nil
618}
619
620// movesFilesOrigins is the set of origins a manifest's objects name, each
621// held to the claim's provenance rule (P6-D3): an immutable object within
622// the authorized incarnation location in this region's bucket, under a
623// generation's files prefix, <location>/files/g-<generation>/. A manifest
624// naming nothing has its own prefix as its one origin. The admitted chain
625// selects which manifest is newest; it does not bound the origins that
626// manifest may name, because AdmitWriter keeps eight admissions while a
627// copied manifest carries its oldest objects for as long as they are live
628// (fix round 1, finding 6). The same location rule is the files live
629// set's (pinFilesLiveSet), so every object a live manifest names stays
630// pinned under it.
631func movesFilesOrigins(location, prefix string, m hostfiles.Manifest) ([]string, error) {
632 base := moves.FilesGenerationBase(location)
633 seen := map[string]bool{}
634 var out []string
635 for _, entries := range []map[string]hostfiles.Entry{m.Entries, m.Platform} {
636 for name, e := range entries {
637 if e.Object == "" || e.Deleted || e.Skipped != "" {
638 continue
639 }
640 origin, _, ok := strings.Cut(e.Object, "objects/")
641 rest, under := strings.CutPrefix(origin, base)
642 gen, slash := strings.CutSuffix(rest, "/")
643 if !ok || !under || !slash || !movesFilesGeneration(gen) {
644 return nil, fmt.Errorf("%w: %s names %s, not an object under a generation's files prefix of %s", hostfiles.ErrOrigin, name, e.Object, location)
645 }
646 if !seen[origin] {
647 seen[origin] = true
648 out = append(out, origin)
649 }
650 }
651 }
652 if len(out) == 0 && prefix != "" {
653 out = []string{prefix}
654 }
655 return out, nil
656}
657
658// openPrepared is the probe's baseline (in.Prepared, or a relocating
659// op's opening of P, preparedOpening): P's prepared manifest at exactly
660// the version its installation's binding names (fix round 1, finding 2).
661// The binding is read by preparedBinding, which chooses its reader by the
662// claim's op kind, and is held to P's key, the claim's op and the
663// installation's stamp; the bytes are read at that version,
664// checked against the bound digest, and validated as a prepared
665// manifest (sequence 0, objects within the location by the claim's rule,
666// movesFilesOrigins: a cross-region landing's names destination-local
667// objects by its build, a same-region relocation's may retain source
668// generation objects). The key must
669// currently hold either those bytes (the first establishment, which the
670// returned publisher replaces by compare-and-swap) or this box's own
671// establishment of them (a retry after a crash past the commit: the same
672// entries, the env bytes on disk, no adoption item, which the publisher
673// then leaves as it is). Anything else refuses before any write.
674func (f *movesFiles) openPrepared(ctx context.Context, files release.Store, o hostclaim.Observed, in hostclaim.Admission, gen, prefix string, env []byte) (*hostfiles.Publisher, error) {
675 if f.DataDir == "" {
676 return nil, hostwrite.ErrUnavailable
677 }
678 h := o.Claim.Holder
679 key := hostfiles.ManifestKey(prefix)
680 pm, err := f.preparedBinding(o, gen, key)
681 if err != nil {
682 return nil, err
683 }
684 exact, err := f.Data.GetVersion(ctx, f.Bucket, key, pm.Version)
685 if err != nil {
686 return nil, fmt.Errorf("host: the prepared files manifest %s@%s: %w", key, pm.Version, err)
687 }
688 if sum := sha256.Sum256(exact); hex.EncodeToString(sum[:]) != pm.SHA256 {
689 return nil, fmt.Errorf("%w: the prepared files manifest %s@%s is not the journalled bytes", hostbox.ErrChanged, key, pm.Version)
690 }
691 prepared, found, err := hostfiles.Load(ctx, exactBytesStore{key: key, body: exact, version: pm.Version}, prefix)
692 if err != nil || !found {
693 return nil, fmt.Errorf("host: the prepared files manifest %s@%s: %v", key, pm.Version, err)
694 }
695 // The prepared manifest's allowed set: the origins its entries
696 // reference, proven, plus P itself (its carried set, seeded by the
697 // build with its own prefix, is a hint only; an old one carries none).
698 if len(prepared.Origins) == 0 {
699 prepared.Origins = []string{prefix}
700 }
701 allowed, err := f.movesFilesAllowed(ctx, o, in, h.Location, gen, prepared)
702 if err != nil {
703 return nil, fmt.Errorf("host: the prepared files manifest %s@%s: %w", key, pm.Version, err)
704 }
705 if prepared.Seq != 0 {
706 return nil, fmt.Errorf("%w: the prepared files manifest %s@%s is at sequence %d, not 0", hostbox.ErrChanged, key, pm.Version, prepared.Seq)
707 }
708 // One read of the key: the bytes and the token the publisher is built
709 // from are the ones checked here, never a second read (fix round 2,
710 // finding 2). A replacement after this read fails the commit's
711 // compare-and-swap.
712 cur, token, err := files.Get(ctx, key)
713 if err != nil {
714 return nil, err
715 }
716 if !bytes.Equal(cur, exact) {
717 current, found, err := hostfiles.Load(ctx, exactBytesStore{key: key, body: cur, version: token}, prefix)
718 if err != nil || !found {
719 return nil, fmt.Errorf("host: the files manifest %s: %v", key, err)
720 }
721 if err := establishedOver(ctx, files, h.Location, prefix, allowed, current, prepared, env); err != nil {
722 return nil, fmt.Errorf("%w: the files manifest %s is neither the prepared manifest %s nor this box's establishment of it: %v", hostbox.ErrChanged, key, pm.Version, err)
723 }
724 }
725 p, err := hostfiles.OpenAt(files, prefix, cur, token)
726 if err != nil {
727 return nil, err
728 }
729 p.SetOrigins(allowed)
730 return p, nil
731}
732
733// preparedBinding is the binding of P's prepared manifest, read by the
734// reader the claim's op kind names, never by which file happens to exist:
735// a move's install journals it in the moves op journal
736// (.moves/<host>/<P>/journal.json, the files_manifest step); a
737// relocation's install records it in its own install journal
738// (relocate/ops/<op>/install.json), which the claim's local_install
739// receipt binds by digest, and the claim's prepared_authority binds its
740// bytes. Either way the binding names key, op, stamp, digest and version.
741// The absence of one reader's file is never permission to use the other's,
742// and any other kind opens no prepared generation.
743func (f *movesFiles) preparedBinding(o hostclaim.Observed, gen, key string) (preparedManifest, error) {
744 switch kind := o.Claim.Op.Kind; {
745 case kind == hostclaim.KindRelocating:
746 return relocationPreparedManifest(f.DataDir, f.Box, o, gen, key)
747 case claimMovesKind(kind):
748 return movesPreparedManifest(f.DataDir, o, gen, key)
749 default:
750 return preparedManifest{}, fmt.Errorf("%w: a %s claim opens no prepared generation", hostbox.ErrChanged, kind)
751 }
752}
753
754// movesPreparedManifest is a move's binding: the op journal's
755// files_manifest step, held to P's key, the claim's op and the
756// installation's stamp (installationStamp).
757func movesPreparedManifest(dataDir string, o hostclaim.Observed, gen, key string) (preparedManifest, error) {
758 j, err := moves.ReadJournal(dataDir, o.Host, gen)
759 if err != nil {
760 return preparedManifest{}, err
761 }
762 raw, ok := j.Steps[landStepFilesManifest]
763 if !ok {
764 return preparedManifest{}, fmt.Errorf("%w: the op journal of %s/%s has no prepared files manifest", errLandingChanged, o.Host, gen)
765 }
766 stamp, err := installationStamp(o.Claim)
767 if err != nil {
768 return preparedManifest{}, err
769 }
770 return decodePreparedManifest(raw, key, o.Claim.Op.ID, stamp)
771}
772
773// relocationPreparedManifest is a relocation's binding (region HA §5.8).
774// It holds two stamps apart, each exactly. P's build stamp is the one the
775// claim's prepared_authority binds (install_stamp when ToInstalled ran):
776// the authority names P, that stamp and the digest of P's prepared files
777// manifest (sequence 0), and the install journal's files_manifest step
778// binds P's key, the op, that build stamp, the digest and the exact
779// version. This box's install stamp is the claim's current install_stamp,
780// which a replacement target issues anew while keeping P and its
781// authority (hostclaim ReplaceTarget at installed, or in probing before
782// P's first admission): the holder's stamp (the probe's admission keeps
783// it), the claim's local_install receipt (this box, that stamp, the
784// digest of its install journal) and the journal itself (read whole, held
785// to the receipt's digest, naming the host, the op, this box, that stamp
786// and P) all carry it. The build precedes every install, so the build
787// stamp is never past the install stamp. The step's digest must be the
788// authority's. Anything missing or different refuses; neither stamp
789// stands in for the other.
790func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen, key string) (preparedManifest, error) {
791 c := o.Claim
792 auth, err := preparedAuthority(c)
793 if err != nil {
794 return preparedManifest{}, err
795 }
796 stamp, err := relocationInstallStamp(c)
797 if err != nil {
798 return preparedManifest{}, err
799 }
800 if auth.Generation != gen || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) || auth.InstallStamp == 0 || auth.InstallStamp > stamp || c.Holder.Stamp != stamp {
801 return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v is not a build of P %s at or before the install stamp %d", hostbox.ErrChanged, auth, gen, stamp)
802 }
803 var receipt hostclaim.LocalInstall
804 if json.Unmarshal([]byte(c.Op.Journal["local_install"]), &receipt) != nil || receipt.Box != box || receipt.InstallStamp != stamp || !claimHexDigest(receipt.JournalSHA256) {
805 return preparedManifest{}, fmt.Errorf("%w: the claim journals no local install receipt of this box at the install stamp", hostbox.ErrChanged)
806 }
807 path, err := relocationInstallPath(dataDir, c.Op.ID)
808 if err != nil {
809 return preparedManifest{}, err
810 }
811 raw, err := os.ReadFile(path)
812 if err != nil {
813 return preparedManifest{}, fmt.Errorf("host: the relocation's install journal: %w", err)
814 }
815 if hostfilesSum(raw) != receipt.JournalSHA256 {
816 return preparedManifest{}, fmt.Errorf("%w: %s is not the install journal the claim's receipt names", hostbox.ErrChanged, path)
817 }
818 var j relocationInstallJournal
819 if err := json.Unmarshal(raw, &j); err != nil {
820 return preparedManifest{}, fmt.Errorf("%w: %s: %v", hostbox.ErrChanged, path, err)
821 }
822 if j.V != 1 || j.Host != o.Host || j.Op != c.Op.ID || j.Box != box || j.InstallStamp != stamp || j.Prepared != gen {
823 return preparedManifest{}, fmt.Errorf("%w: %s is not this box's install of %s at the install stamp", hostbox.ErrChanged, path, gen)
824 }
825 step, ok := j.Steps[relocationStepFilesManifest]
826 if !ok {
827 return preparedManifest{}, fmt.Errorf("%w: the install journal of %s has no prepared files manifest", errLandingChanged, c.Op.ID)
828 }
829 pm, err := decodePreparedManifest(step, key, c.Op.ID, auth.InstallStamp)
830 if err != nil {
831 return preparedManifest{}, err
832 }
833 if pm.SHA256 != auth.FilesManifestSHA256 {
834 return preparedManifest{}, fmt.Errorf("%w: the install bound %s at %s, not the prepared authority's digest", hostbox.ErrChanged, key, pm.SHA256)
835 }
836 return pm, nil
837}
838
839// preparedOpening reports whether this admission opens a prepared
840// generation whose files manifest an installation built. For a move it is
841// in.Prepared. For a relocation it is the probe's admission that opens the
842// journalled P, whether or not the database is empty: relocation always
843// builds P's files manifest (§5.8), so database emptiness (in.Empty) and
844// files preparation are independent, and an empty database still starts
845// from P's files. A relocating admission that says prepared or empty for
846// any other generation is refused.
847func preparedOpening(o hostclaim.Observed, in ClaimLaunchInputs) (bool, error) {
848 c := o.Claim
849 if c.Op.Kind != hostclaim.KindRelocating {
850 return in.Prepared, nil
851 }
852 p := c.Op.Journal["prepared"]
853 opens := p != "" && c.Op.Phase == hostclaim.PhaseRelocatingProbing && in.Admission.NewGeneration == p
854 if (in.Prepared || in.Empty) && !opens {
855 return false, fmt.Errorf("%w: a relocating admission is prepared or empty only when it opens P", hostbox.ErrChanged)
856 }
857 return opens, nil
858}
859
860// installationStamp is the stamp the installation that wrote P's prepared
861// manifest captured (ir.capture at local), validated apart from the
862// admitted session's authority (whole-branch fixes, item 2). A return's
863// install (restoring, or a revert's) ran at the source installation's
864// stamp S, which the claim journals at the flip (source_stamp) and the
865// recovery's cold admission then replaces with a new one; a landing's
866// install ran at the flip's stamp, which the first probe's admission
867// keeps (KeepStamp, since no writer was admitted between the installing
868// CAS and the probe), so it is the holder's.
869func installationStamp(c *hostclaim.Claim) (uint64, error) {
870 switch c.Op.Kind {
871 case hostclaim.KindRestoring, hostclaim.KindReverting:
872 stamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64)
873 if err != nil || stamp == 0 {
874 return 0, fmt.Errorf("%w: the claim journals no source installation stamp for %s", errLandingChanged, c.Op.ID)
875 }
876 return stamp, nil
877 }
878 if c.Holder == nil || c.Holder.Stamp == 0 {
879 return 0, fmt.Errorf("%w: the holder has no installation stamp", errLandingChanged)
880 }
881 return c.Holder.Stamp, nil
882}
883
884// establishedOver reports whether cur is this box's establishment of
885// prepared, in full (fix round 2 finding 6, fix round 3): exactly what
886// this establishment would have written, derived from the prepared
887// baseline and the establishment's own operations by the publisher's
888// rules, never from the replay's own numbers. From prepared's sequence,
889// the env file's bytes take a new object at the next sequence when they
890// differ from prepared's env item; the adoption item's deletion takes the
891// sequence after when prepared carried one; the manifest's sequence is
892// the last handed out; the entries and other platform items are
893// prepared's; the carried set is the admitted one. cur must equal that
894// manifest field for field, and the env object must be at its key with
895// the env's digest. A replay whose env entry names an object elsewhere, a
896// different object under P, or a fabricated sequence whose object does
897// not exist, is not this box's establishment however its digest reads.
898func establishedOver(ctx context.Context, files release.Store, location, prefix string, allowed []string, cur, prepared hostfiles.Manifest, env []byte) error {
899 want := hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}, Origins: allowed}
900 for k, v := range prepared.Entries {
901 want.Entries[k] = v
902 }
903 for k, v := range prepared.Platform {
904 want.Platform[k] = v
905 }
906 next := prepared.Seq
907 envSHA := hostfilesSum(env)
908 if e, ok := prepared.Platform[hostfiles.EnvItem]; !ok || e.SHA256 != envSHA {
909 next++
910 want.Platform[hostfiles.EnvItem] = hostfiles.Entry{Object: fmt.Sprintf("%sobjects/%016d-%s", prefix, next, envSHA), SHA256: envSHA, Size: int64(len(env)), Mode: 0o600, Seq: next}
911 }
912 if _, has := prepared.Platform[instancekey.AdoptionItem]; has {
913 next++
914 delete(want.Platform, instancekey.AdoptionItem)
915 }
916 want.Seq = next
917 if cur.Entries == nil {
918 cur.Entries = map[string]hostfiles.Entry{}
919 }
920 if cur.Platform == nil {
921 cur.Platform = map[string]hostfiles.Entry{}
922 }
923 if !reflect.DeepEqual(cur, want) {
924 return fmt.Errorf("the manifest %+v is not what this establishment writes over the prepared one (%+v)", cur, want)
925 }
926 if _, err := movesFilesOrigins(location, prefix, cur); err != nil {
927 return err
928 }
929 if err := hostfiles.ValidateManifest(cur, allowed); err != nil {
930 return err
931 }
932 e := want.Platform[hostfiles.EnvItem]
933 body, _, err := files.Get(ctx, e.Object)
934 if err != nil {
935 return fmt.Errorf("the env object %s: %w", e.Object, err)
936 }
937 if hostfilesSum(body) != e.SHA256 || int64(len(body)) != e.Size {
938 return fmt.Errorf("the env object %s is not the env file's bytes", e.Object)
939 }
940 return nil
941}
942
943func hostfilesSum(body []byte) string {
944 sum := sha256.Sum256(body)
945 return hex.EncodeToString(sum[:])
946}
947
948// exactBytesStore is a read-only release.Store holding one object's exact
949// bytes at a version already read: hostfiles.Load's reader for a manifest
950// the caller verified.
951type exactBytesStore struct {
952 key, version string
953 body []byte
954}
955
956func (s exactBytesStore) Get(_ context.Context, key string) ([]byte, string, error) {
957 if key != s.key {
958 return nil, "", os.ErrNotExist
959 }
960 return s.body, s.version, nil
961}
962func (exactBytesStore) Put(context.Context, string, []byte, string) (string, error) {
963 return "", errors.New("host: the prepared manifest's exact bytes are read only")
964}
965func (exactBytesStore) List(context.Context, string) ([]string, error) {
966 return nil, errors.New("host: the prepared manifest's exact bytes are read only")
967}
968func (exactBytesStore) ListDirs(context.Context, string) ([]string, error) {
969 return nil, errors.New("host: the prepared manifest's exact bytes are read only")
970}
971func (exactBytesStore) Delete(context.Context, string) error {
972 return errors.New("host: the prepared manifest's exact bytes are read only")
973}
974
975// selectionStore is the data bucket as Establish writes it (fix round 1,
976// finding 7): every write is fenced by the start's captured selection
977// immediately before the request (work.With with no effect, the
978// writerFence idiom: the lock is taken for the check and released before
979// the request), and every request is bounded by hostbox.RequestLimit. No
980// request is made under the lock, so a stalled request never delays
981// BeginFence; a selection that no longer holds refuses the write.
982type selectionStore struct {
983 release.Store
984 fence func(context.Context) error
985}
986
987func (s selectionStore) bound(ctx context.Context) (context.Context, context.CancelFunc) {
988 return context.WithTimeout(ctx, hostbox.RequestLimit)
989}
990
991func (s selectionStore) Get(ctx context.Context, key string) ([]byte, string, error) {
992 ctx, cancel := s.bound(ctx)
993 defer cancel()
994 return s.Store.Get(ctx, key)
995}
996
997func (s selectionStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
998 if err := s.fence(ctx); err != nil {
999 return "", err
1000 }
1001 ctx, cancel := s.bound(ctx)
1002 defer cancel()
1003 return s.Store.Put(ctx, key, body, ifMatch)
1004}
1005
1006// emptyManifest is the base of an empty start.
1007func emptyManifest() hostfiles.Manifest {
1008 return hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}
1009}
1010
1011// Establish is ClaimFilesManifest.Establish. It runs once per start under
1012// the start's captured selection, after the launch source has minted the
1013// instance key into <host>.env and before the app executes. The env file
1014// is the one the launch source minted into, found through the host's row
1015// (EnvFile; a nested row's is in its instance directory).
1016//
1017// The baseline:
1018// - a prepared opening (preparedOpening: in.Prepared for a move, the
1019// probe's admission opening P for a relocation, whatever its
1020// database): P's own prepared manifest at the version its
1021// installation bound (preparedBinding, by the op's kind), read current
1022// with its etag so the env revision replaces exactly it (a new
1023// version; the sequence-0 version stays), validated against the
1024// origins its entries name;
1025// - in.Empty (the claim's empty bootstrap, EmptyAuthority already run):
1026// no manifest;
1027// - otherwise the newest valid manifest of the chain, read by exact
1028// version; with none, the pin's never-published evidence or an error
1029// (hostfiles.ErrNoEvidence), never an empty bootstrap.
1030//
1031// Then the env file's exact bytes on disk are staged as the EnvItem, the
1032// adoption item is dropped and the revision is committed (create-only for
1033// a new generation; compare-and-swap on P), each bucket write fenced by
1034// the start's selection immediately before it and bounded by the request
1035// limit, never under the lock the work holds (selectionStore; fix round
1036// 1, finding 7). Then, under work, the local adoption record is removed:
1037// the one local effect. Establish never writes the env file. An error, a
1038// bucket that cannot be reached included, fails the wake.
1039func (f *movesFiles) Establish(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
1040 if f == nil || f.Files == nil || f.Data == nil || f.Bucket == "" || f.Region == "" || f.Box == "" || f.EnvFile == nil || ctx == nil || work == nil {
1041 return hostwrite.ErrUnavailable
1042 }
1043 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Host == "" || in.Identity.Config == "" || in.Identity.Release == "" {
1044 return hostbox.ErrChanged
1045 }
1046 h := o.Claim.Holder
1047 gen := in.Admission.NewGeneration
1048 if in.Admission.Box != f.Box || !regionname.SameRegion(f.HomeRegion, in.Admission.Region, f.Region) || !movesFilesGeneration(gen) || h.Location == "" {
1049 return hostbox.ErrChanged
1050 }
1051 envFile, err := f.EnvFile(o.Host, in.DBPath)
1052 if err != nil {
1053 return err
1054 }
1055 env, err := readClaimEnvFile(envFile)
1056 if err != nil {
1057 return fmt.Errorf("host: the env file the files manifest must carry: %w", err)
1058 }
1059 prepared, err := preparedOpening(o, in)
1060 if err != nil {
1061 return err
1062 }
1063
1064 prefix := movesFilesPrefix(h.Location, gen)
1065 var storage release.Store = f.Files
1066 if f.Seed != nil {
1067 if err := f.Seed.check(ctx, o, in); err != nil {
1068 return err
1069 }
1070 storage = filesSeedCheckingStore{Store: f.Files, data: f.Data, bucket: f.Bucket, prefix: prefix}
1071 }
1072 files := selectionStore{Store: storage, fence: writerFence(work, in.Identity)}
1073 var p *hostfiles.Publisher
1074 switch {
1075 case prepared:
1076 p, err = f.openPrepared(ctx, files, o, in.Admission, gen, prefix, env)
1077 if err != nil {
1078 return err
1079 }
1080 case in.Empty:
1081 own, err := f.proveOrigins(ctx, o, in.Admission, h.Location, []string{gen})
1082 if err != nil {
1083 return err
1084 }
1085 p, err = hostfiles.NewFromCopy(files, prefix, emptyManifest(), nil)
1086 if err != nil {
1087 return err
1088 }
1089 p.SetOrigins(own)
1090 default:
1091 // The chain is the holder's: the newest authorized manifest through
1092 // it wins, a recovery's own included (the prepared rollback
1093 // generation's, or a later recovery publication's). Only when that
1094 // chain holds no manifest, the first recovery admission, whose
1095 // predecessor is the target's generation in the other region, does
1096 // src_gen bridge the missing source baseline (item 3 as re-ruled;
1097 // the database selector's distinction, movesrecoveryowners.go
1098 // baseline: the recorded source restore only while the recovery has
1099 // no root of its own).
1100 base, origins, found, err := f.newestManifest(ctx, o, in.Admission, h.Location, movesFilesChain(h, f.HomeRegion, f.Region))
1101 if err != nil {
1102 return err
1103 }
1104 if !found && restoresSourceBaseline(o.Claim) {
1105 src, _ := recoverySourceGeneration(o.Claim)
1106 base, origins, found, err = f.newestManifest(ctx, o, in.Admission, h.Location, movesFilesChainFrom(h, src, f.HomeRegion, f.Region))
1107 if err != nil {
1108 return err
1109 }
1110 }
1111 if !found {
1112 never := false
1113 if f.Evidence != nil && f.Seed == nil {
1114 pin, ok, err := f.Evidence(ctx, o)
1115 if err != nil {
1116 return fmt.Errorf("host: the claim pin's files evidence: %w", err)
1117 }
1118 never = ok && pin.NeverPublished()
1119 }
1120 if !never && f.Seed == nil {
1121 return fmt.Errorf("%w: no files manifest in the admitted predecessor chain of %s (%s)", hostfiles.ErrNoEvidence, o.Host, h.Generation)
1122 }
1123 own, err := f.proveOrigins(ctx, o, in.Admission, h.Location, []string{gen})
1124 if err != nil {
1125 return err
1126 }
1127 base, origins = emptyManifest(), own
1128 }
1129 p, err = hostfiles.NewFromCopy(files, prefix, base, origins)
1130 if err != nil {
1131 return err
1132 }
1133 p.SetOrigins(origins)
1134 }
1135 if f.Seed != nil {
1136 if err := f.Seed.stage(ctx, o, in, work, p, envFile); err != nil {
1137 return err
1138 }
1139 }
1140 if err := p.StagePlatform(ctx, hostfiles.EnvItem, env); err != nil {
1141 return err
1142 }
1143 if err := p.StagePlatformDelete(instancekey.AdoptionItem); err != nil {
1144 return err
1145 }
1146 if f.Seed != nil {
1147 if err := f.Seed.check(ctx, o, in); err != nil {
1148 return err
1149 }
1150 }
1151 if _, err := p.Commit(ctx); err != nil {
1152 return err
1153 }
1154 return work.With(ctx, in.Identity, func() error {
1155 return removeClaimAdoption(envFile, o.Host+instancekey.AdoptionSuffix)
1156 })
1157}
1158
1159// readClaimEnvFile reads the env file's exact bytes: in a nested instance
1160// directory, which the tenant can write, only beneath it (readInstanceFile:
1161// no link at any component, a regular file, at most the env read limit),
1162// and a missing file is an error either way.
1163func readClaimEnvFile(e claimEnvFile) ([]byte, error) {
1164 if !e.Nested {
1165 return os.ReadFile(e.Path())
1166 }
1167 env, absent, err := readInstanceFile(e.Dir, e.Name, instancekey.EnvReadLimit)
1168 if err == nil && absent {
1169 err = fmt.Errorf("%s: %w", e.Path(), fs.ErrNotExist)
1170 }
1171 return env, err
1172}
1173
1174// removeClaimAdoption removes the adoption record beside the env file, a
1175// nested one beneath the instance directory (instancefs.RemoveBeneath). A
1176// record that is not there is already removed.
1177func removeClaimAdoption(e claimEnvFile, name string) error {
1178 var err error
1179 if e.Nested {
1180 err = instancefs.RemoveBeneath(e.Dir, name)
1181 } else {
1182 err = os.Remove(filepath.Join(e.Dir, name))
1183 }
1184 if err != nil && !errors.Is(err, fs.ErrNotExist) {
1185 return err
1186 }
1187 return nil
1188}
1189