package hostfiles import ( "bytes" "context" "encoding/json" "errors" "fmt" "strings" "github.com/carlosframework/platform/internal/release" ) // This file holds what a cold admission and an installation share with // region HA's relocation (stage B, region-ha-1b-next-build 73bab739): the // one rule for a host that never published files, and the create-only // prepared manifest an installation writes before any session exists // (claim rev 6.30 R18-1, moves spec §7.6). Stage B rebases // relocationPlatformItem onto PlatformItem; the values it reads from its // files pin are PinnedFiles. // PinnedFiles is what a pin says about a host's published files: the // digest of the pinned manifest and the pinner's retained copy of it. The // pinner writes both for a host that published files and neither for one // that never did (a holder no writer was ever admitted to, or a release // flush that published nothing); a pin with one and not the other is // half-made and refused before it reaches here (ReadPin, H2). type PinnedFiles struct { SHA256 string Retained string } // NeverPublished reports the pinner's positive evidence that the host // never published files: neither a digest nor a retained copy. It is the // only evidence this package accepts for an empty files state; a missing // manifest, a missing pin or a missing item is never evidence (ruling 68 // as amended in Task 8c: absence is never evidence). func (p PinnedFiles) NeverPublished() bool { return p.SHA256 == "" && p.Retained == "" } // ErrNoEvidence marks a manifest without a platform item under a pin that // does not evidence a host that never published files. Platform items have // no tombstone (StagePlatformDelete drops the item), so a host whose env // file was deleted reads the same as one whose env file was never // published, and a manifest no release flush wrote (a failover's) carries // no wake state however many timers the host had. Only the pin can tell // them apart. var ErrNoEvidence = errors.New("hostfiles: the manifest has no such platform item, and the pin does not evidence a host that never published files") // ErrTombstone marks a platform item recorded as deleted, which no // publisher writes. var ErrTombstone = errors.New("hostfiles: the manifest holds a tombstone for a platform item, which no publisher writes") // PlatformItem reports whether m carries the platform item name (the env // file, the wake state) for a reader to restore, or the host has none. A // live item is the host's. A manifest without it is "none" only when pin // evidences a host that never published files (NeverPublished). Any other // manifest without the item is ErrNoEvidence; a tombstone is ErrTombstone // under either pin. func PlatformItem(pin PinnedFiles, m Manifest, name string) (bool, error) { e, ok := m.Platform[name] switch { case ok && !e.Deleted: return true, nil case ok: return false, fmt.Errorf("%w: %s", ErrTombstone, name) case pin.NeverPublished(): return false, nil default: return false, fmt.Errorf("%w: %s", ErrNoEvidence, name) } } // Prepared builds the prepared manifest of a generation no session has // opened: an installation's initial files state, written under the // authority of its operation and installation stamp, not a session. Every // content object is create-only at sequence 0 under the prepared prefix, // keyed by its digest, and Commit writes one create-only manifest at // sequence 0. Nothing is ever replaced: a replay finds each object by its // key (the same digest) and the manifest by its key, reads it back, and // succeeds only when the bytes are the ones it would have written. The // session-bound publisher later opens the prefix, takes this manifest as // its baseline and continues from sequence 1. // // The binding to {location, generation, op id, installation stamp} is the // caller's: the location and generation are the prefix, and the op and // stamp are journalled beside the manifest's digest by the installation // that wrote it. The manifest itself carries no such fields: a reader that // copies it must not reinterpret it, and the pinned bytes of a relocation // compare equal to the prepared copy entry for entry (stage B). type Prepared struct { s release.Store prefix string man Manifest spent bool } // NewPrepared returns a builder for prefix. It reads nothing: a manifest // already at the prefix is found by Commit. func NewPrepared(s release.Store, prefix string) (*Prepared, error) { if s == nil { return nil, errors.New("hostfiles: prepared manifest: no store") } if err := checkPrefix(prefix); err != nil { return nil, err } return &Prepared{s: s, prefix: prefix, man: Manifest{V: ManifestVersion, Entries: map[string]Entry{}, Platform: map[string]Entry{}}}, nil } // put writes body as a create-only object at sequence 0 under the prepared // prefix. An object already at that key is the same bytes: the key is the // digest. func (p *Prepared) put(ctx context.Context, body []byte, mode uint32) (Entry, error) { if p.spent { return Entry{}, errors.New("hostfiles: prepared manifest already committed") } if int64(len(body)) > MaxFileBytes { return Entry{}, fmt.Errorf("hostfiles: %d bytes is over the %d-byte limit", len(body), MaxFileBytes) } sha := sum(body) key := p.prefix + objectKey(0, sha) if _, err := p.s.Put(ctx, key, body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) { return Entry{}, err } return Entry{Object: key, SHA256: sha, Size: int64(len(body)), Mode: mode & 0o777, Seq: 0}, nil } // Stage records path's content. A file over MaxFileBytes is recorded as // skipped, as SyncDir records it, so the manifest still names every file. func (p *Prepared) Stage(ctx context.Context, path string, body []byte, mode uint32) error { clean, err := CleanPath(path) if err != nil { return err } if err := withinTreeCap(clean); err != nil { return err } if p.spent { return errors.New("hostfiles: prepared manifest already committed") } if int64(len(body)) > MaxFileBytes { p.man.Entries[clean] = Entry{Seq: 0, Skipped: fmt.Sprintf("%d bytes is over the %d-byte limit", len(body), MaxFileBytes)} return nil } e, err := p.put(ctx, body, mode) if err != nil { return err } p.man.Entries[clean] = e return nil } // StageSkipped records that path exists but was not published, and why: // a file the builder did not read because its size is over the limit (the // caller decides from the stat, before any read). func (p *Prepared) StageSkipped(path, why string) error { clean, err := CleanPath(path) if err != nil { return err } if err := withinTreeCap(clean); err != nil { return err } if p.spent { return errors.New("hostfiles: prepared manifest already committed") } p.man.Entries[clean] = Entry{Seq: 0, Skipped: why} return nil } // StagePlatform records a platform-managed item (the env file, the wake // state). func (p *Prepared) StagePlatform(ctx context.Context, name string, body []byte) error { if name == "" || strings.ContainsAny(name, "/\x00") { return fmt.Errorf("hostfiles: platform item %q is not a plain name", name) } e, err := p.put(ctx, body, 0o600) if err != nil { return err } p.man.Platform[name] = e return nil } // Commit writes the one prepared manifest, create-only, and returns it // with its exact bytes and the version the store answered for them (the // caller journals the digest and the version, so a later reader takes // exactly this revision). A manifest already at the key is read back: the // same bytes are a replay and success, with the version the key holds; // any other manifest is ErrManifestExists and nothing is written. After // Commit the builder is spent. func (p *Prepared) Commit(ctx context.Context) (Manifest, []byte, string, error) { if p.spent { return Manifest{}, nil, "", errors.New("hostfiles: prepared manifest already committed") } p.man.Seq = 0 p.man.Origins = []string{p.prefix} body, err := json.Marshal(p.man) if err != nil { return Manifest{}, nil, "", fmt.Errorf("hostfiles: marshal manifest: %w", err) } version, err := p.s.Put(ctx, ManifestKey(p.prefix), body, "") if errors.Is(err, release.ErrCASConflict) { cur, curVersion, gerr := p.s.Get(ctx, ManifestKey(p.prefix)) if gerr != nil { return Manifest{}, nil, "", gerr } if !bytes.Equal(cur, body) { return Manifest{}, nil, "", fmt.Errorf("%w: %s", ErrManifestExists, p.prefix) } version = curVersion } else if err != nil { return Manifest{}, nil, "", err } p.spent = true return p.man, body, version, nil } // NewFromCopy is a publisher for a generation that has no manifest yet, // seeded with base: a validated manifest of an admitted predecessor, read // by the caller at its exact version, or an empty one for the claim's own // empty bootstrap or a host the pin evidences never published files. Every // entry keeps its origin-qualified object (nothing is uploaded again) and // the copy carries base's origin set (the caller sets the new one with // SetOrigins); new objects go under prefix from base.Seq+1; and the first Commit is // create-only, so a repeat reads its own bytes back and another manifest // at the prefix is ErrSuperseded, never replaced. It is CopyManifest with // the caller's own changes in the same revision: a cold admission's // establishment stages the env file and drops the adoption item before it // commits (claim §6.2a P6-D3; instance key O1'). // // origins are the prefixes base's entries may reference (nil or empty: // prefix alone). base is validated before anything is written. func NewFromCopy(s release.Store, prefix string, base Manifest, origins []string) (*Publisher, error) { if s == nil { return nil, errors.New("hostfiles: copy: no store") } if err := checkPrefix(prefix); err != nil { return nil, err } origins, err := admitted(prefix, origins) if err != nil { return nil, err } if base.V != ManifestVersion { return nil, fmt.Errorf("hostfiles: manifest is version %d; this binary reads %d", base.V, ManifestVersion) } if err := checkManifest(base, origins); err != nil { return nil, err } man := Manifest{V: ManifestVersion, Seq: base.Seq, Entries: map[string]Entry{}, Platform: map[string]Entry{}, Origins: append([]string(nil), base.Origins...)} for k, e := range base.Entries { man.Entries[k] = e } for k, e := range base.Platform { man.Platform[k] = e } return &Publisher{s: s, prefix: prefix, man: man, next: base.Seq, dirty: true}, nil }