carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostrelocate
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "sort"
12 "strings"
13 "time"
14
15 "github.com/carlosframework/platform/internal/genselect"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostfiles"
18 "github.com/carlosframework/platform/internal/instancekey"
19 "github.com/carlosframework/platform/internal/moves"
20 "github.com/carlosframework/platform/internal/park"
21 "github.com/carlosframework/platform/internal/release"
22)
23
24// PinObject is the immutable pinned recovery set (§5.8): the database
25// plan genselect selected, with every file's exact version, and the files
26// pin, the newest manifest in the admitted chain, by digest (spec §18: the
27// store has no version-id read, so content is identified by SHA-256).
28// ClaimGen is the claim generation the set was selected against; the
29// object's key names it (hostclaim.RecoveryPinKey), so a pin whose journal
30// CAS lost is never reused. DB is nil for an evidenced empty bootstrap.
31type PinObject struct {
32 V int `json:"v"`
33 Host string `json:"host"`
34 Op string `json:"op"`
35 ClaimGen uint64 `json:"claim_gen"`
36 DB *genselect.Plan `json:"db,omitempty"`
37 Empty string `json:"empty,omitempty"` // "release" | "session-0": the evidence
38 Files FilesPin `json:"files"`
39 PinnedAt time.Time `json:"pinned_at"`
40 // Release is the app release version the relocation installs, read
41 // at pinning from StorePinner.Release (R9); a reuse carries it.
42 Release string `json:"release"`
43 // InstanceKey is the key a nonempty database is sealed with when the
44 // pinned manifest evidences it by an adoption record rather than by
45 // instance.key (O2', instance-key plan rev 3): its digest and source.
46 // The install delivers exactly this key or refuses (O3').
47 InstanceKey *KeyPin `json:"instance_key,omitempty"`
48}
49
50// KeyPin is a pinned instance key: the SHA-256 of the key and the file
51// that supplies it at launch (instancekey.SourcePlatform or SourceConfig).
52type KeyPin struct {
53 Source string `json:"source"`
54 SHA256 string `json:"sha256"`
55}
56
57// FilesPin names one generation's files manifest by prefix, sequence and
58// the digest of its exact bytes, and the origins its entries may name:
59// every generation prefix in the admitted chain as observed at the pin,
60// the manifest's own included (contracts note §1).
61type FilesPin struct {
62 Prefix string `json:"prefix"`
63 Seq int64 `json:"seq"`
64 SHA256 string `json:"sha256"`
65 Origins []string `json:"origins"`
66 // Retained is the pinner's immutable copy of the exact manifest bytes
67 // (RetainedManifestKey), written create-only before the pin object. A
68 // late revision of the source manifest changes nothing the build
69 // reads: P is written from this copy, never from the source again.
70 // Empty for a host that never published files.
71 Retained string `json:"retained,omitempty"`
72}
73
74// RetainedManifestKey is where the pinner keeps the pinned manifest's
75// bytes: control/relocations/<host>/<op>/manifest-<sha256>.json.
76func RetainedManifestKey(host, op, sha string) string {
77 return "control/relocations/" + host + "/" + op + "/manifest-" + sha + ".json"
78}
79
80// PinKey is hostclaim.RecoveryPinKey.
81func PinKey(host, op string, gen uint64) string { return hostclaim.RecoveryPinKey(host, op, gen) }
82
83// StorePinner selects the pinned recovery set against the claim
84// observation it is given (§5.8 "The pinned recovery set") and writes it
85// create-only at the key for that observation's claim generation. It
86// checks every bound JournalPin will check before it writes anything, so
87// a doomed pin is never published; a retry at the same generation reuses
88// its own object unchanged. The caller holds the location lease (claim §4:
89// "New pins must be established under the required leases").
90type StorePinner struct {
91 Claims release.Store // control bucket: pin objects
92 Data release.Store // the region's data bucket: files manifests
93 SelectDB func(context.Context, genselect.Request) (*genselect.Plan, error)
94 Bucket string // the region's data bucket name, for genselect
95 Region string
96 // HomeRegion is the box's verified home binding, or "": genselect sees
97 // the holder's admissions through it (park.HolderRegionView), as main's
98 // serving baseline does, so an admission recorded under the home's
99 // other name is this region's.
100 HomeRegion string
101 // Release is the app release version the host's relocation installs,
102 // read from the region controller's authoritative registry route for
103 // the host (registry.Route.Version, the route's deployed version) at
104 // pinning (R9 on amadan #236). It is never read from the box, the
105 // request or the box's install journal, and nothing else in the pin
106 // supplies it: the installing CAS copies it to install_release, which
107 // moves' held-stage relay requires the request's version to equal.
108 // Stage B wires it to the controller's registry. Nil is ErrUnsupported;
109 // an empty version refuses the pin, never a guess.
110 Release func(ctx context.Context, host string) (string, error)
111}
112
113func (p StorePinner) Pin(ctx context.Context, obs hostclaim.Observed, now time.Time) (hostclaim.RecoveryPin, error) {
114 if p.Claims == nil || p.Data == nil || p.SelectDB == nil || p.Release == nil || p.Bucket == "" || p.Region == "" || obs.Claim == nil || obs.Claim.Holder == nil {
115 return hostclaim.RecoveryPin{}, ErrUnsupported
116 }
117 c := obs.Claim
118 key := PinKey(c.Host, c.Op.ID, c.Gen)
119 if raw, _, err := p.Claims.Get(ctx, key); err == nil {
120 return p.reuse(c, key, raw)
121 } else if !errors.Is(err, os.ErrNotExist) {
122 return hostclaim.RecoveryPin{}, err
123 }
124 var rel hostclaim.Release
125 released := json.Unmarshal([]byte(c.Op.Journal["release"]), &rel) == nil && rel.ReleasedBy != ""
126 for k, at := range c.Op.Journal {
127 if len(k) > len(".observed_at") && k[len(k)-len(".observed_at"):] == ".observed_at" {
128 t, err := time.Parse(time.RFC3339Nano, at)
129 if err != nil {
130 return hostclaim.RecoveryPin{}, err
131 }
132 if now.Before(t.Add(hostclaim.FailoverPinDelay)) {
133 return hostclaim.RecoveryPin{}, ErrNotYet
134 }
135 }
136 }
137 version, err := p.Release(ctx, c.Host)
138 if err != nil {
139 return hostclaim.RecoveryPin{}, err
140 }
141 if version == "" {
142 return hostclaim.RecoveryPin{}, fmt.Errorf("%w: the registry route for %s names no release", ErrRefused, c.Host)
143 }
144 obj := PinObject{V: 1, Host: c.Host, Op: c.Op.ID, ClaimGen: c.Gen, PinnedAt: now.UTC(), Release: version}
145 plan, err := p.SelectDB(ctx, genselect.Request{Bucket: p.Bucket, Region: p.Region, Host: c.Host, Holder: park.HolderRegionView(*c.Holder, p.HomeRegion, p.Region)})
146 switch {
147 case err == nil:
148 obj.DB = plan
149 case errors.Is(err, genselect.ErrEmpty) && released && rel.FinalTXID == 0:
150 obj.Empty = "release" // the stopped writer flushed and reports position 0
151 case errors.Is(err, genselect.ErrEmpty) && !released && c.Holder.Session == 0:
152 obj.Empty = "session-0" // no writer was ever admitted, so nothing was written
153 default:
154 // A listing that finds nothing after a writer ran is not evidence
155 // of an empty database (§5.8, claim §6.4a): refuse, never bootstrap.
156 return hostclaim.RecoveryPin{}, err
157 }
158 if released && obj.DB != nil && obj.DB.MaxTXID < rel.FinalTXID {
159 return hostclaim.RecoveryPin{}, fmt.Errorf("%w: the listing is at %d, the release at %d", ErrNotYet, obj.DB.MaxTXID, rel.FinalTXID)
160 }
161 // A nonempty database written by an admitted writer is sealed with
162 // that writer's instance.key: the pinned manifest must carry it (rev
163 // 18, round 17 finding 1).
164 needKey := c.Holder.Session != 0 && obj.DB != nil && obj.DB.MaxTXID > 0
165 // The writer whose data is pinned: the session admitted as the pinned
166 // database's generation (O2', instance-key plan ruling 20). Asked only
167 // when instance.key is missing.
168 writer := func() (instancekey.Binding, error) { return pinnedWriter(c, obj.DB.Generation) }
169 files, keyPin, retained, err := p.filesPin(ctx, writer, c.Holder, released && rel.ManifestSeq == 0, needKey)
170 if err != nil {
171 return hostclaim.RecoveryPin{}, err
172 }
173 obj.Files, obj.InstanceKey = files, keyPin
174 if released && obj.Files.Seq < rel.ManifestSeq {
175 return hostclaim.RecoveryPin{}, fmt.Errorf("%w: the files manifest is at %d, the release at %d", ErrNotYet, obj.Files.Seq, rel.ManifestSeq)
176 }
177 if retained != nil {
178 // The exact bytes, kept where nothing rewrites them, before the
179 // pin that names them exists.
180 obj.Files.Retained = RetainedManifestKey(c.Host, c.Op.ID, files.SHA256)
181 if err := release.CreateExact(ctx, p.Claims, obj.Files.Retained, retained); err != nil && !errors.Is(err, release.ErrCASConflict) {
182 return hostclaim.RecoveryPin{}, err
183 }
184 }
185 raw, err := json.Marshal(obj)
186 if err != nil {
187 return hostclaim.RecoveryPin{}, err
188 }
189 if err := release.CreateExact(ctx, p.Claims, key, raw); err != nil {
190 if !errors.Is(err, release.ErrCASConflict) {
191 return hostclaim.RecoveryPin{}, err
192 }
193 got, _, gerr := p.Claims.Get(ctx, key) // another worker pinned this generation first
194 if gerr != nil {
195 return hostclaim.RecoveryPin{}, gerr
196 }
197 return p.reuse(c, key, got)
198 }
199 return pinRef(key, raw, obj), nil
200}
201
202func (p StorePinner) reuse(c *hostclaim.Claim, key string, raw []byte) (hostclaim.RecoveryPin, error) {
203 var obj PinObject
204 if err := json.Unmarshal(raw, &obj); err != nil || obj.V != 1 || obj.Host != c.Host || obj.Op != c.Op.ID || obj.ClaimGen != c.Gen || (obj.DB == nil) == (obj.Empty == "") || obj.Release == "" {
205 return hostclaim.RecoveryPin{}, fmt.Errorf("%w: pin object %s is not this operation's at this generation", ErrRefused, key)
206 }
207 return pinRef(key, raw, obj), nil
208}
209
210func pinRef(key string, raw []byte, obj PinObject) hostclaim.RecoveryPin {
211 sum := sha256.Sum256(raw)
212 ref := hostclaim.RecoveryPin{Key: key, SHA256: hex.EncodeToString(sum[:]), FilesManifest: hostfiles.ManifestKey(obj.Files.Prefix), FilesSeq: obj.Files.Seq, PinnedAt: obj.PinnedAt, Empty: obj.DB == nil, Release: obj.Release}
213 if obj.DB != nil {
214 ref.Generation, ref.MaxTXID = obj.DB.Generation, obj.DB.MaxTXID
215 }
216 return ref
217}
218
219// InstanceKeyPath is H6's always-continuous instance.key, at the root of
220// the host's files directory.
221const InstanceKeyPath = "instance.key"
222
223func liveEntry(m hostfiles.Manifest, path string) bool {
224 e, ok := m.Entries[path]
225 return ok && !e.Deleted && e.Skipped == "" && e.Object != ""
226}
227
228// ErrFilesUnevidenced is a pin refused because its files set cannot be
229// shown to hold the host's platform files: a writer was admitted and no
230// files manifest exists in the admitted chain (rev 17), or the database
231// is nonempty and the pinned manifest has no instance.key (rev 18). The
232// worker marks the operation stuck.
233var ErrFilesUnevidenced = errors.New("hostrelocate: the files evidence does not cover the host's platform files")
234
235// filesPin is §5.6's files pin: the manifest of holder.generation as
236// observed or, if that generation has none, of the nearest predecessor in
237// holder.admissions that has one. It is chosen independently of the
238// database baseline (B2-4). A read error refuses; no manifest anywhere in
239// the chain is a host that never published files, pinned at sequence 0.
240// Origins is the retained chain plus every generation of this incarnation
241// whose objects the pinned manifest references (rev 12, round 11): the
242// claim's §6.2a files live set is the objects the live manifests
243// reference, independent of holder.admissions, which AdmitWriter trims to
244// eight. An unchanged file published at g1 and carried by every later copy
245// stays restorable at g10. Every referenced object must lie under this
246// holder's location (its incarnation) in this region's data bucket, at
247// files/g-<generation>/objects/ (moves.GenerationFilesPrefix, the files
248// prefix rule the C8 provider and the park view's files live set share);
249// any other reference refuses the pin.
250func (p StorePinner) filesPin(ctx context.Context, writer func() (instancekey.Binding, error), h *hostclaim.Holder, releasedEmpty, needKey bool) (FilesPin, *KeyPin, []byte, error) {
251 chain := []string{h.Generation}
252 for i := len(h.Admissions) - 1; i >= 0; i-- {
253 chain = append(chain, h.Admissions[i].Generation, h.Admissions[i].Predecessor)
254 }
255 seen := map[string]bool{}
256 var origins []string
257 for _, gen := range chain {
258 if gen == "" || gen == hostclaim.RootGeneration || seen[gen] {
259 continue
260 }
261 seen[gen] = true
262 origins = append(origins, moves.GenerationFilesPrefix(h.Location, gen))
263 }
264 for _, prefix := range origins {
265 raw, _, err := p.Data.Get(ctx, hostfiles.ManifestKey(prefix))
266 if errors.Is(err, os.ErrNotExist) {
267 continue
268 }
269 if err != nil {
270 return FilesPin{}, nil, nil, err
271 }
272 var m hostfiles.Manifest // decoded from the bytes the digest names
273 if err := json.Unmarshal(raw, &m); err != nil || m.V != hostfiles.ManifestVersion {
274 return FilesPin{}, nil, nil, errors.Join(fmt.Errorf("%w: files manifest %s", ErrRefused, prefix), err)
275 }
276 if origins, err = referencedOrigins(h.Location, origins, m); err != nil {
277 return FilesPin{}, nil, nil, err
278 }
279 // A pinned manifest carries the env file and the wake state
280 // (Task 8c, ruling 68 amended; the install's own rule,
281 // relocationPlatformItem): a host that published files has no
282 // evidence that it has neither. Platform items have no tombstone
283 // (StagePlatformDelete drops the item), so a missing one reads the
284 // same as one never published, and a manifest no release flush
285 // wrote (a failover's) carries no wake state however many timers
286 // the host had. Refused here, before any build, rather than at the
287 // target's install.
288 for _, name := range []string{hostfiles.EnvItem, hostfiles.WakeItem} {
289 if e, ok := m.Platform[name]; !ok || e.Deleted {
290 return FilesPin{}, nil, nil, fmt.Errorf("%w: %s's manifest has no %s", ErrFilesUnevidenced, prefix, name)
291 }
292 }
293 var keyPin *KeyPin
294 if needKey && !liveEntry(m, InstanceKeyPath) {
295 // (a) failed: no instance.key. (b'): an adoption record bound
296 // to the pinned writer, for the key the target will deliver
297 // (O2', instance-key plan rev 3). Without it the manifest may
298 // have been copied before the app minted a key of its own
299 // (§5.6's continuous-sync window), and installing this set
300 // would mint a new key over data sealed with the old one.
301 w, err := writer()
302 if err != nil {
303 return FilesPin{}, nil, nil, err
304 }
305 if keyPin, err = p.adoptedKey(ctx, w, prefix, m, origins); err != nil {
306 return FilesPin{}, nil, nil, err
307 }
308 }
309 sum := sha256.Sum256(raw)
310 return FilesPin{Prefix: prefix, Seq: m.Seq, SHA256: hex.EncodeToString(sum[:]), Origins: origins}, keyPin, raw, nil
311 }
312 // No manifest anywhere in the chain. An empty files pin needs the
313 // same kind of evidence an empty database pin does (ruling 15): no
314 // writer was ever admitted on this holder (session 0), so no app ever
315 // ran to mint instance.key, report.token or an env (H6); or the
316 // stopped writer's release flush, which publishes every file whatever
317 // the setting, reported nothing (manifest_seq 0). Any other admitted
318 // writer without a manifest (an adopted host whose files never
319 // published, a first manifest that never landed, a trimmed chain)
320 // refuses: the worker marks the operation stuck for an operator (rev
321 // 17, finding F2), never an install that mints a fresh key over data
322 // sealed with the old one.
323 if h.Session != 0 && !releasedEmpty {
324 return FilesPin{}, nil, nil, fmt.Errorf("%w: %s has admitted a writer but no files manifest exists in its chain", ErrFilesUnevidenced, h.Location)
325 }
326 gen := h.Generation
327 if gen == "" {
328 gen = hostclaim.RootGeneration
329 }
330 prefix := moves.GenerationFilesPrefix(h.Location, gen)
331 return FilesPin{Prefix: prefix, Origins: []string{prefix}}, nil, nil, nil
332}
333
334// referencedOrigins adds to origins the files prefix of every generation
335// whose objects m references, each of which must be an object this
336// incarnation's location published: <location>/files/g-<generation>/objects/
337// (moves.GenerationFilesPrefix).
338func referencedOrigins(location string, origins []string, m hostfiles.Manifest) ([]string, error) {
339 base := moves.FilesGenerationBase(location)
340 have := make(map[string]bool, len(origins))
341 for _, o := range origins {
342 have[o] = true
343 }
344 var extra []string
345 check := func(name string, e hostfiles.Entry) error {
346 if e.Object == "" {
347 return nil // a tombstone or a skipped entry names no object
348 }
349 gen, rest, ok := strings.Cut(strings.TrimPrefix(e.Object, base), "/")
350 if !strings.HasPrefix(e.Object, base) || !ok || gen == "" || gen == "." || gen == ".." || !strings.HasPrefix(rest, "objects/") {
351 return fmt.Errorf("%w: %s references %s, outside %s: %w", ErrRefused, name, e.Object, base, hostfiles.ErrOrigin)
352 }
353 if o := moves.GenerationFilesPrefix(location, gen); !have[o] {
354 have[o] = true
355 extra = append(extra, o)
356 }
357 return nil
358 }
359 for name, e := range m.Entries {
360 if err := check(name, e); err != nil {
361 return nil, err
362 }
363 }
364 for name, e := range m.Platform {
365 if err := check(name, e); err != nil {
366 return nil, err
367 }
368 }
369 sort.Strings(extra)
370 return append(origins, extra...), nil
371}
372
373// ReadPin reads and verifies the pin object a journaled RecoveryPin names:
374// the exact bytes hash to the journaled digest (ruling 5), and the
375// journaled release is the object's. Every reader of a pin goes through it.
376func ReadPin(ctx context.Context, claims release.Store, ref hostclaim.RecoveryPin) (PinObject, error) {
377 raw, _, err := claims.Get(ctx, ref.Key)
378 if err != nil {
379 return PinObject{}, err
380 }
381 if sum := sha256.Sum256(raw); hex.EncodeToString(sum[:]) != ref.SHA256 {
382 return PinObject{}, fmt.Errorf("%w: pin object %s does not hash to its journaled digest", ErrRefused, ref.Key)
383 }
384 var obj PinObject
385 if err := json.Unmarshal(raw, &obj); err != nil || obj.V != 1 {
386 return PinObject{}, fmt.Errorf("%w: pin object %s", ErrRefused, ref.Key)
387 }
388 // The journal's copy of the release is what install_release copies
389 // (R9): it must be the object's own.
390 if obj.Release == "" || obj.Release != ref.Release {
391 return PinObject{}, fmt.Errorf("%w: pin object %s names release %q, the journal %q", ErrRefused, ref.Key, obj.Release, ref.Release)
392 }
393 // A pin without a database is an empty bootstrap only by its named
394 // evidence, and the journal's copy must agree: a missing plan is never
395 // read as an empty database (absence is never evidence).
396 if (obj.DB == nil) == (obj.Empty == "") || ref.Empty != (obj.DB == nil) {
397 return PinObject{}, fmt.Errorf("%w: pin object %s does not say whether its database is empty", ErrRefused, ref.Key)
398 }
399 // Likewise a files pin is a host that published no files only when it
400 // names neither a digest nor a retained copy: one without the other is
401 // never read as an empty manifest.
402 if (obj.Files.SHA256 == "") != (obj.Files.Retained == "") {
403 return PinObject{}, fmt.Errorf("%w: pin object %s names a files digest or a retained copy without the other", ErrRefused, ref.Key)
404 }
405 return obj, nil
406}
407
408// CopyPinnedManifest writes the pinned files manifest into toPrefix
409// (§5.8 "write P's files manifest, create-only, as the pinned manifest's
410// entries"), from the pinner's retained copy of its exact bytes (claims:
411// the control bucket), never from the source prefix again: a late
412// revision of the source manifest cannot move the pin (B2-4). The bytes
413// must hash to the pin's digest and sit at the pinned sequence; every
414// entry is validated against the pinned origins (L13); the write is
415// create-only, and a retry finding the same bytes at toPrefix succeeds.
416// A host that never published files gets an empty manifest.
417func CopyPinnedManifest(ctx context.Context, claims, data release.Store, pin FilesPin, toPrefix string) (hostfiles.Manifest, error) {
418 if pin.Prefix == "" || toPrefix == "" || len(pin.Origins) == 0 || (pin.SHA256 == "") != (pin.Retained == "") {
419 return hostfiles.Manifest{}, ErrUnsupported
420 }
421 var pinned hostfiles.Manifest
422 if pin.Retained == "" {
423 // A host that never published files: P starts an empty manifest.
424 pinned = hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}
425 } else {
426 raw, _, err := claims.Get(ctx, pin.Retained)
427 if err != nil {
428 return hostfiles.Manifest{}, err
429 }
430 if sum := sha256.Sum256(raw); hex.EncodeToString(sum[:]) != pin.SHA256 {
431 return hostfiles.Manifest{}, fmt.Errorf("%w: retained manifest %s is not the pinned bytes", ErrRefused, pin.Retained)
432 }
433 if err := json.Unmarshal(raw, &pinned); err != nil || pinned.Seq != pin.Seq {
434 return hostfiles.Manifest{}, fmt.Errorf("%w: retained manifest %s", ErrRefused, pin.Retained)
435 }
436 if err := hostfiles.ValidateManifest(pinned, pin.Origins); err != nil {
437 return hostfiles.Manifest{}, err
438 }
439 }
440 // P's prepared manifest (claim rev 6.30 R18-1, moves rev 7.22 §7.6):
441 // the pinned entries, by their exact content-addressed objects, at
442 // sequence 0. The source's sequence is provenance (the pin keeps it),
443 // never P's: the session publisher starts at 1 after the first
444 // admission. The binding {location, P, op, install stamp} is the
445 // prefix (location, P) and the claim's prepared_authority (op, stamp,
446 // this manifest's digest).
447 pinned.Seq = 0
448 body, err := json.Marshal(pinned)
449 if err != nil {
450 return hostfiles.Manifest{}, err
451 }
452 if err := release.CreateExact(ctx, data, hostfiles.ManifestKey(toPrefix), body); err != nil {
453 if errors.Is(err, release.ErrCASConflict) {
454 return hostfiles.Manifest{}, hostfiles.ErrManifestExists
455 }
456 return hostfiles.Manifest{}, err
457 }
458 return pinned, nil
459}
460
461// pinnedWriter is the writer whose data a pin of generation gen holds: the
462// holder's own session when gen is the holder's generation, else the
463// admitted session recorded for gen. A generation no admission names has
464// no writer to bind evidence to, and refuses.
465func pinnedWriter(c *hostclaim.Claim, gen string) (instancekey.Binding, error) {
466 h := c.Holder
467 w := instancekey.Binding{Host: c.Host, Incarnation: h.Incarnation, Generation: gen}
468 if gen == h.Generation {
469 w.Session = h.Session
470 }
471 for i := len(h.Admissions) - 1; w.Session == 0 && i >= 0; i-- {
472 if h.Admissions[i].Generation == gen {
473 w.Session = h.Admissions[i].Session
474 }
475 }
476 if w.Session == 0 || w.Incarnation == "" || gen == "" {
477 return instancekey.Binding{}, fmt.Errorf("%w: no admitted writer for generation %q", ErrFilesUnevidenced, gen)
478 }
479 return w, nil
480}
481
482// adoptedKey is O2' source (b') (instance-key plan rev 3, ruling 20): the
483// pinned manifest's adoption record must bind exactly the pinned writer,
484// and name the key the target will deliver. A platform-sourced record must
485// match the env item's own key, which the install restores byte for byte.
486// A config-sourced record needs an env item with no own key (one would
487// override the configuration at launch); its digest is pinned, and the
488// install verifies the configuration it renders still delivers it (O3').
489// The env item is parsed with instancekey.ParseEnv, the activator's own
490// rule. A named item that cannot be read is an error, never "no evidence".
491func (p StorePinner) adoptedKey(ctx context.Context, writer instancekey.Binding, prefix string, m hostfiles.Manifest, origins []string) (*KeyPin, error) {
492 read := func(name string) ([]byte, bool, error) {
493 if e, ok := m.Platform[name]; !ok || e.Deleted {
494 return nil, false, nil
495 }
496 raw, err := hostfiles.ReadPlatform(ctx, p.Data, prefix, m, name, origins)
497 return raw, err == nil, err
498 }
499 rec, ok, err := read(instancekey.AdoptionItem)
500 if err != nil {
501 return nil, err
502 }
503 refuse := func(why string) (*KeyPin, error) {
504 return nil, fmt.Errorf("%w: %s's pinned manifest has no %s and %s, for a nonempty database", ErrFilesUnevidenced, prefix, InstanceKeyPath, why)
505 }
506 if !ok {
507 return refuse("no adoption record")
508 }
509 a, err := instancekey.ParseAdoption(rec)
510 if err != nil {
511 return refuse("its adoption record is malformed")
512 }
513 if !a.Binds(writer) {
514 return refuse("its adoption record is not the pinned writer's")
515 }
516 env, haveEnv, err := read(hostfiles.EnvItem)
517 if err != nil {
518 return nil, err
519 }
520 own, _ := instancekey.Effective(nil, instancekey.ParseEnv(env))
521 // A configured key needs the env item, present and readable, with no
522 // key of its own: a manifest without host.env is not evidence that
523 // nothing overrides the configuration (absence is never evidence).
524 // Residual: a source with no host.env item and a configured key
525 // refuses, because platform items have no tombstone (StagePlatformDelete
526 // drops the item), so a deleted env reads as one never published. That
527 // is rare only for a source that released normally: its release flush
528 // stages <host>.env, which holds CARLOS_ADMIN_TOKEN on every exec
529 // instance. After a box loss no release flush ran, and the item is
530 // there only if O1' (the C8 provider's Establish) published <host>.env
531 // at the writer's admission.
532 switch {
533 case a.Source == instancekey.SourcePlatform && own != "" && instancekey.Digest(own) == a.SHA256:
534 case a.Source == instancekey.SourceConfig && haveEnv && own == "":
535 default:
536 return refuse("its adoption record does not name the key the env delivers")
537 }
538 return &KeyPin{Source: a.Source, SHA256: a.SHA256}, nil
539}
540