carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "strings"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// This file holds what a cold admission and an installation share with
15// region HA's relocation (stage B, region-ha-1b-next-build 73bab739): the
16// one rule for a host that never published files, and the create-only
17// prepared manifest an installation writes before any session exists
18// (claim rev 6.30 R18-1, moves spec §7.6). Stage B rebases
19// relocationPlatformItem onto PlatformItem; the values it reads from its
20// files pin are PinnedFiles.
21
22// PinnedFiles is what a pin says about a host's published files: the
23// digest of the pinned manifest and the pinner's retained copy of it. The
24// pinner writes both for a host that published files and neither for one
25// that never did (a holder no writer was ever admitted to, or a release
26// flush that published nothing); a pin with one and not the other is
27// half-made and refused before it reaches here (ReadPin, H2).
28type PinnedFiles struct {
29 SHA256 string
30 Retained string
31}
32
33// NeverPublished reports the pinner's positive evidence that the host
34// never published files: neither a digest nor a retained copy. It is the
35// only evidence this package accepts for an empty files state; a missing
36// manifest, a missing pin or a missing item is never evidence (ruling 68
37// as amended in Task 8c: absence is never evidence).
38func (p PinnedFiles) NeverPublished() bool { return p.SHA256 == "" && p.Retained == "" }
39
40// ErrNoEvidence marks a manifest without a platform item under a pin that
41// does not evidence a host that never published files. Platform items have
42// no tombstone (StagePlatformDelete drops the item), so a host whose env
43// file was deleted reads the same as one whose env file was never
44// published, and a manifest no release flush wrote (a failover's) carries
45// no wake state however many timers the host had. Only the pin can tell
46// them apart.
47var ErrNoEvidence = errors.New("hostfiles: the manifest has no such platform item, and the pin does not evidence a host that never published files")
48
49// ErrTombstone marks a platform item recorded as deleted, which no
50// publisher writes.
51var ErrTombstone = errors.New("hostfiles: the manifest holds a tombstone for a platform item, which no publisher writes")
52
53// PlatformItem reports whether m carries the platform item name (the env
54// file, the wake state) for a reader to restore, or the host has none. A
55// live item is the host's. A manifest without it is "none" only when pin
56// evidences a host that never published files (NeverPublished). Any other
57// manifest without the item is ErrNoEvidence; a tombstone is ErrTombstone
58// under either pin.
59func PlatformItem(pin PinnedFiles, m Manifest, name string) (bool, error) {
60 e, ok := m.Platform[name]
61 switch {
62 case ok && !e.Deleted:
63 return true, nil
64 case ok:
65 return false, fmt.Errorf("%w: %s", ErrTombstone, name)
66 case pin.NeverPublished():
67 return false, nil
68 default:
69 return false, fmt.Errorf("%w: %s", ErrNoEvidence, name)
70 }
71}
72
73// Prepared builds the prepared manifest of a generation no session has
74// opened: an installation's initial files state, written under the
75// authority of its operation and installation stamp, not a session. Every
76// content object is create-only at sequence 0 under the prepared prefix,
77// keyed by its digest, and Commit writes one create-only manifest at
78// sequence 0. Nothing is ever replaced: a replay finds each object by its
79// key (the same digest) and the manifest by its key, reads it back, and
80// succeeds only when the bytes are the ones it would have written. The
81// session-bound publisher later opens the prefix, takes this manifest as
82// its baseline and continues from sequence 1.
83//
84// The binding to {location, generation, op id, installation stamp} is the
85// caller's: the location and generation are the prefix, and the op and
86// stamp are journalled beside the manifest's digest by the installation
87// that wrote it. The manifest itself carries no such fields: a reader that
88// copies it must not reinterpret it, and the pinned bytes of a relocation
89// compare equal to the prepared copy entry for entry (stage B).
90type Prepared struct {
91 s release.Store
92 prefix string
93 man Manifest
94 spent bool
95}
96
97// NewPrepared returns a builder for prefix. It reads nothing: a manifest
98// already at the prefix is found by Commit.
99func NewPrepared(s release.Store, prefix string) (*Prepared, error) {
100 if s == nil {
101 return nil, errors.New("hostfiles: prepared manifest: no store")
102 }
103 if err := checkPrefix(prefix); err != nil {
104 return nil, err
105 }
106 return &Prepared{s: s, prefix: prefix, man: Manifest{V: ManifestVersion, Entries: map[string]Entry{}, Platform: map[string]Entry{}}}, nil
107}
108
109// put writes body as a create-only object at sequence 0 under the prepared
110// prefix. An object already at that key is the same bytes: the key is the
111// digest.
112func (p *Prepared) put(ctx context.Context, body []byte, mode uint32) (Entry, error) {
113 if p.spent {
114 return Entry{}, errors.New("hostfiles: prepared manifest already committed")
115 }
116 if int64(len(body)) > MaxFileBytes {
117 return Entry{}, fmt.Errorf("hostfiles: %d bytes is over the %d-byte limit", len(body), MaxFileBytes)
118 }
119 sha := sum(body)
120 key := p.prefix + objectKey(0, sha)
121 if _, err := p.s.Put(ctx, key, body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) {
122 return Entry{}, err
123 }
124 return Entry{Object: key, SHA256: sha, Size: int64(len(body)), Mode: mode & 0o777, Seq: 0}, nil
125}
126
127// Stage records path's content. A file over MaxFileBytes is recorded as
128// skipped, as SyncDir records it, so the manifest still names every file.
129func (p *Prepared) Stage(ctx context.Context, path string, body []byte, mode uint32) error {
130 clean, err := CleanPath(path)
131 if err != nil {
132 return err
133 }
134 if err := withinTreeCap(clean); err != nil {
135 return err
136 }
137 if p.spent {
138 return errors.New("hostfiles: prepared manifest already committed")
139 }
140 if int64(len(body)) > MaxFileBytes {
141 p.man.Entries[clean] = Entry{Seq: 0, Skipped: fmt.Sprintf("%d bytes is over the %d-byte limit", len(body), MaxFileBytes)}
142 return nil
143 }
144 e, err := p.put(ctx, body, mode)
145 if err != nil {
146 return err
147 }
148 p.man.Entries[clean] = e
149 return nil
150}
151
152// StageSkipped records that path exists but was not published, and why:
153// a file the builder did not read because its size is over the limit (the
154// caller decides from the stat, before any read).
155func (p *Prepared) StageSkipped(path, why string) error {
156 clean, err := CleanPath(path)
157 if err != nil {
158 return err
159 }
160 if err := withinTreeCap(clean); err != nil {
161 return err
162 }
163 if p.spent {
164 return errors.New("hostfiles: prepared manifest already committed")
165 }
166 p.man.Entries[clean] = Entry{Seq: 0, Skipped: why}
167 return nil
168}
169
170// StagePlatform records a platform-managed item (the env file, the wake
171// state).
172func (p *Prepared) StagePlatform(ctx context.Context, name string, body []byte) error {
173 if name == "" || strings.ContainsAny(name, "/\x00") {
174 return fmt.Errorf("hostfiles: platform item %q is not a plain name", name)
175 }
176 e, err := p.put(ctx, body, 0o600)
177 if err != nil {
178 return err
179 }
180 p.man.Platform[name] = e
181 return nil
182}
183
184// Commit writes the one prepared manifest, create-only, and returns it
185// with its exact bytes and the version the store answered for them (the
186// caller journals the digest and the version, so a later reader takes
187// exactly this revision). A manifest already at the key is read back: the
188// same bytes are a replay and success, with the version the key holds;
189// any other manifest is ErrManifestExists and nothing is written. After
190// Commit the builder is spent.
191func (p *Prepared) Commit(ctx context.Context) (Manifest, []byte, string, error) {
192 if p.spent {
193 return Manifest{}, nil, "", errors.New("hostfiles: prepared manifest already committed")
194 }
195 p.man.Seq = 0
196 p.man.Origins = []string{p.prefix}
197 body, err := json.Marshal(p.man)
198 if err != nil {
199 return Manifest{}, nil, "", fmt.Errorf("hostfiles: marshal manifest: %w", err)
200 }
201 version, err := p.s.Put(ctx, ManifestKey(p.prefix), body, "")
202 if errors.Is(err, release.ErrCASConflict) {
203 cur, curVersion, gerr := p.s.Get(ctx, ManifestKey(p.prefix))
204 if gerr != nil {
205 return Manifest{}, nil, "", gerr
206 }
207 if !bytes.Equal(cur, body) {
208 return Manifest{}, nil, "", fmt.Errorf("%w: %s", ErrManifestExists, p.prefix)
209 }
210 version = curVersion
211 } else if err != nil {
212 return Manifest{}, nil, "", err
213 }
214 p.spent = true
215 return p.man, body, version, nil
216}
217
218// NewFromCopy is a publisher for a generation that has no manifest yet,
219// seeded with base: a validated manifest of an admitted predecessor, read
220// by the caller at its exact version, or an empty one for the claim's own
221// empty bootstrap or a host the pin evidences never published files. Every
222// entry keeps its origin-qualified object (nothing is uploaded again) and
223// the copy carries base's origin set (the caller sets the new one with
224// SetOrigins); new objects go under prefix from base.Seq+1; and the first Commit is
225// create-only, so a repeat reads its own bytes back and another manifest
226// at the prefix is ErrSuperseded, never replaced. It is CopyManifest with
227// the caller's own changes in the same revision: a cold admission's
228// establishment stages the env file and drops the adoption item before it
229// commits (claim §6.2a P6-D3; instance key O1').
230//
231// origins are the prefixes base's entries may reference (nil or empty:
232// prefix alone). base is validated before anything is written.
233func NewFromCopy(s release.Store, prefix string, base Manifest, origins []string) (*Publisher, error) {
234 if s == nil {
235 return nil, errors.New("hostfiles: copy: no store")
236 }
237 if err := checkPrefix(prefix); err != nil {
238 return nil, err
239 }
240 origins, err := admitted(prefix, origins)
241 if err != nil {
242 return nil, err
243 }
244 if base.V != ManifestVersion {
245 return nil, fmt.Errorf("hostfiles: manifest is version %d; this binary reads %d", base.V, ManifestVersion)
246 }
247 if err := checkManifest(base, origins); err != nil {
248 return nil, err
249 }
250 man := Manifest{V: ManifestVersion, Seq: base.Seq, Entries: map[string]Entry{}, Platform: map[string]Entry{}, Origins: append([]string(nil), base.Origins...)}
251 for k, e := range base.Entries {
252 man.Entries[k] = e
253 }
254 for k, e := range base.Platform {
255 man.Platform[k] = e
256 }
257 return &Publisher{s: s, prefix: prefix, man: man, next: base.Seq, dirty: true}, nil
258}
259