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 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "slices"
13 "strconv"
14
15 "github.com/carlosframework/platform/internal/appconfig"
16 "github.com/carlosframework/platform/internal/hostbox"
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/hostfiles"
19 "github.com/carlosframework/platform/internal/hostguard"
20 "github.com/carlosframework/platform/internal/hostops"
21 "github.com/carlosframework/platform/internal/hostrelocate"
22 "github.com/carlosframework/platform/internal/hostwrite"
23 "github.com/carlosframework/platform/internal/instancefs"
24 "github.com/carlosframework/platform/internal/instancekey"
25 "github.com/carlosframework/platform/internal/lease"
26 "github.com/carlosframework/platform/internal/moves"
27 "github.com/carlosframework/platform/internal/provision"
28 "github.com/carlosframework/platform/internal/registry"
29 "github.com/carlosframework/platform/internal/release"
30)
31
32// The install authority's actions (ruling 46): the target's guard
33// activation, its build writes and its local install; its held config;
34// its held release. The claim's Policy admits none of them for relocating
35// (R1); relocationInstallPolicy does, on main's WorkSource, until claim
36// ask C9 moves them into the claim.
37const (
38 relocationActionInstall = "install"
39 relocationActionHeldConfig = "held-config"
40 relocationActionHeldRelease = "held-release"
41)
42
43func relocationInstallStamp(c *hostclaim.Claim) (uint64, error) {
44 stamp, err := strconv.ParseUint(c.Op.Journal["install_stamp"], 10, 64)
45 if err != nil || stamp == 0 {
46 return 0, fmt.Errorf("%w: no install stamp", hostrelocate.ErrRefused)
47 }
48 return stamp, nil
49}
50
51// relocationInstallAuthority is the install's claim check (rulings 43, 46,
52// 52): this box executes this relocating op at one of phases, P unopened,
53// not stuck, not lost, and the holder's stamp is the journaled install
54// stamp.
55func relocationInstallAuthority(c *hostclaim.Claim, op, box string, phases ...string) error {
56 if c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindRelocating || c.Op.ID != op || c.Op.Exec != box || c.Op.Journal["stuck"] != "" ||
57 slices.Contains(c.Lost, box) || !slices.Contains(phases, c.Op.Phase) || hostclaim.PreparedOpened(c) {
58 return fmt.Errorf("%w: the install's authority changed", hostrelocate.ErrRefused)
59 }
60 stamp, err := relocationInstallStamp(c)
61 if err != nil {
62 return err
63 }
64 if c.Holder.Stamp != stamp {
65 return fmt.Errorf("%w: the holder's stamp is not the install stamp", hostrelocate.ErrRefused)
66 }
67 return nil
68}
69
70// relocationInstallLocal is main's Local check (identity, quarantine, the
71// participation slot's binding, deadline and loss latch) for an install
72// action on c at captured: the one Selection both the guard activation
73// and every build write ask with.
74func relocationInstallLocal(ctx context.Context, src hostbox.WorkSource, c *hostclaim.Claim, captured hostguard.Captured) error {
75 sel := hostbox.Selection{HomeRegion: src.HomeRegion, FleetHomeRegion: src.FleetHomeRegion, Host: c.Host, Action: relocationActionInstall, Box: src.Box, Region: src.Region, Captured: captured,
76 Kind: c.Op.Kind, Op: c.Op.ID, Phase: c.Op.Phase, Generation: c.Holder.Generation, Session: c.Holder.Session}
77 return src.Local(ctx, c, sel)
78}
79
80// relocationActivationReader validates every fresh read the guard makes
81// while it activates the target's installation (ruling 43), as reconcile's
82// activationReader does for its own.
83type relocationActivationReader struct {
84 claims release.Store
85 check func(context.Context, *hostclaim.Claim) error
86}
87
88func (r relocationActivationReader) Read(ctx context.Context, host string) (hostclaim.Observed, error) {
89 obs, err := hostclaim.Read(ctx, r.claims, host)
90 if err != nil {
91 return obs, err
92 }
93 if obs.Absent || obs.Claim == nil {
94 return hostclaim.Observed{}, hostbox.ErrChanged
95 }
96 if err := r.check(ctx, obs.Claim); err != nil {
97 return hostclaim.Observed{}, err
98 }
99 return obs, nil
100}
101
102// relocationActivate is ruling 43: the first act of installing, before the
103// remote build reads hostguard.Capture. It activates this box's guard
104// record at install_stamp (Activate takes holder.stamp, which the
105// installing CAS set to install_stamp), creating the record on an empty
106// target, under a reader that refuses any read that is not this box's
107// installing op at that stamp or that main's Local check refuses.
108// Idempotent on the same stamp; a retired stamp is ErrRetiredStamp, never
109// reactivated (ruling 52).
110func (a ClaimAssembly) relocationActivate(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
111 return a.relocationActivateAt(cfg, hostclaim.PhaseInstalling)
112}
113
114// relocationActivateInstalled is the same activation for a target that
115// resumes at installed without having built P (Astra round 8): a
116// replacement named at installed, or in probing before P's first
117// admission, which hostclaim.ReplaceTarget leaves at installed under a new
118// install stamp, with P and its prepared authority kept and no build to
119// run. The local install activates its unopened installation at that
120// stamp before it captures it, under the build's reader and checks with
121// the phase widened to exactly installed and probing, P not opened
122// (relocationInstallAuthority refuses an opened P), this box the executor
123// and the named current target, and the holder's stamp the install stamp.
124// For the original target, activated by its build at the same stamp, it
125// is the guard's idempotent no-op. A retired or older stamp is never
126// reactivated (ErrRetiredStamp, ErrOldStamp).
127func (a ClaimAssembly) relocationActivateInstalled(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
128 return a.relocationActivateAt(cfg, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing)
129}
130
131// relocationActivateAt is the one activation: this box's guard record at
132// install_stamp, every fresh read the guard makes held to this box's op at
133// one of phases (relocationInstallAuthority: the executor, not stuck, not
134// lost, P not opened, the holder's stamp the install stamp), this box the
135// named current target, and main's Local check at that installation.
136func (a ClaimAssembly) relocationActivateAt(cfg RelocationAssemblyConfig, phases ...string) func(context.Context, hostclaim.Observed) (hostguard.Captured, error) {
137 return func(ctx context.Context, o hostclaim.Observed) (hostguard.Captured, error) {
138 src := cfg.Source
139 if src.Local == nil || src.Box == "" || o.Claim == nil || o.Claim.Holder == nil {
140 return hostguard.Captured{}, ErrClaimStopUnavailable
141 }
142 op := o.Claim.Op.ID
143 authority := func(c *hostclaim.Claim) error {
144 if err := relocationInstallAuthority(c, op, src.Box, phases...); err != nil {
145 return err
146 }
147 if c.Op.Intent["target_box"] != src.Box {
148 return fmt.Errorf("%w: %s is not the named target", hostrelocate.ErrRefused, src.Box)
149 }
150 return nil
151 }
152 guard := a.Guard
153 guard.Claims = relocationActivationReader{claims: a.Claims, check: func(ctx context.Context, c *hostclaim.Claim) error {
154 if err := authority(c); err != nil {
155 return err
156 }
157 return relocationInstallLocal(ctx, src, c, hostguard.Captured{Incarnation: c.Holder.Incarnation, Stamp: c.Holder.Stamp})
158 }}
159 if a.Reconcile != nil && a.Reconcile.Inventory.Remote() {
160 if err := authority(o.Claim); err != nil {
161 return hostguard.Captured{}, err
162 }
163 rec, err := relocationRecord(ctx, cfg.DataDir, a.Claims, o)
164 if err != nil {
165 return hostguard.Captured{}, err
166 }
167 _, etag, err := a.Claims.Get(ctx, rec.Key)
168 if err != nil {
169 return hostguard.Captured{}, err
170 }
171 selected := hostbox.RecordSelection{Host: o.Host, Key: rec.Key, ETag: etag, Incarnation: o.Claim.Holder.Incarnation, Family: rec.Family, Layout: "nested"}
172 if err = a.Reconcile.PrepareInstallation(ctx, selected, hostguard.Captured{Incarnation: o.Claim.Holder.Incarnation, Stamp: o.Claim.Holder.Stamp}); err != nil {
173 return hostguard.Captured{}, err
174 }
175 }
176 if err := guard.Activate(ctx, o.Host, src.Box, nil); err != nil {
177 return hostguard.Captured{}, err
178 }
179 captured, err := hostguard.Capture(a.Guard.DataDir, o.Host)
180 if err != nil {
181 return hostguard.Captured{}, err
182 }
183 stamp, err := relocationInstallStamp(o.Claim)
184 if err != nil {
185 return hostguard.Captured{}, err
186 }
187 if captured.Stamp != stamp || captured.Incarnation != o.Claim.Holder.Incarnation {
188 return hostguard.Captured{}, hostguard.ErrStampMismatch
189 }
190 return captured, nil
191 }
192}
193
194// relocationBuildAuthority is the remote build's check before every
195// durable write (ruling 59): the claim still runs this box's installing op
196// at the install stamp with the same P and pin, the guard still captures
197// that installation, and main's Local still admits it. Local is the
198// execution lease's own check, so an execution lease lost while the
199// replica lease, claim and guard are unchanged stops the next write.
200func (a ClaimAssembly) relocationBuildAuthority(cfg RelocationAssemblyConfig, o hostclaim.Observed, captured hostguard.Captured) func(context.Context) error {
201 return func(ctx context.Context) error {
202 src := cfg.Source
203 if src.Local == nil || src.Box == "" || o.Claim == nil {
204 return ErrClaimStopUnavailable
205 }
206 fresh, err := hostclaim.Read(ctx, a.Claims, o.Host)
207 if err != nil {
208 return err
209 }
210 c := fresh.Claim
211 if err := relocationInstallAuthority(c, o.Claim.Op.ID, src.Box, hostclaim.PhaseInstalling); err != nil {
212 return err
213 }
214 if c.Op.Journal["prepared"] != o.Claim.Op.Journal["prepared"] || c.Op.Journal["pin"] != o.Claim.Op.Journal["pin"] {
215 return fmt.Errorf("%w: the build's P or pin changed", hostrelocate.ErrRefused)
216 }
217 now, err := hostguard.Capture(a.Guard.DataDir, o.Host)
218 if err != nil {
219 return err
220 }
221 if now != captured || captured.Stamp != c.Holder.Stamp {
222 return fmt.Errorf("%w: this box's installation is not the one the installing CAS issued (%w)", hostrelocate.ErrRefused, hostguard.ErrStampMismatch)
223 }
224 return relocationInstallLocal(ctx, src, c, captured)
225 }
226}
227
228// relocationInstallJournal is the box's durable record of its install for
229// one operation; its digest is the receipt's journal_sha256.
230type relocationInstallJournal struct {
231 V int `json:"v"`
232 Host string `json:"host"`
233 Op string `json:"op"`
234 Box string `json:"box"`
235 Enrollment string `json:"enrollment"`
236 InstallStamp uint64 `json:"install_stamp"`
237 Prepared string `json:"prepared"`
238 Steps map[string]string `json:"steps"` // row, files_manifest, files, env, wake, render, key, materialize
239}
240
241// relocationStepFilesManifest is the install journal's binding of P's
242// prepared files manifest (JSON preparedManifest: key, sha256, version,
243// op, stamp): what the probe's C8 establishment consumes as its baseline,
244// read by exactly that version (relocationPreparedManifest), as a move's
245// op journal binds its own (landStepFilesManifest). Its stamp is P's build
246// stamp, the one the claim's prepared_authority binds (install_stamp when
247// ToInstalled ran), not this box's install stamp: a replacement target
248// keeps P and its authority under a new install stamp (hostclaim
249// ReplaceTarget at installed, or in probing before P's first admission),
250// and its own install journal records its own stamp beside this binding.
251const relocationStepFilesManifest = "files_manifest"
252
253// relocationPreparedBinding binds P's prepared files manifest for the
254// probe's establishment: the current version of P's manifest key as the
255// versioned listing names it, read by that exact version, which must be a
256// prepared manifest (sequence 0) with exactly the entries the install
257// judged (loaded) and the digest the claim's prepared authority (auth)
258// verified, bound to the op and P's build stamp (auth.InstallStamp). A
259// missing, deleted or unversioned manifest, one that is not what the
260// install judged, or an authority that is not P's, refuses.
261func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, bucket, prefix, op string, auth hostclaim.PreparedAuthority, loaded hostfiles.Manifest) (preparedManifest, error) {
262 if data == nil || bucket == "" {
263 return preparedManifest{}, ErrClaimStopUnavailable
264 }
265 if auth.InstallStamp == 0 || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) {
266 return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v binds no build of P's files manifest", hostrelocate.ErrRefused, auth)
267 }
268 key := hostfiles.ManifestKey(prefix)
269 vs, err := data.ListVersions(ctx, bucket, key)
270 if err != nil {
271 return preparedManifest{}, err
272 }
273 version := ""
274 for _, v := range vs {
275 if v.Key != key || !v.IsLatest {
276 continue
277 }
278 if v.DeleteMarker || v.VersionID == "" || v.VersionID == "null" {
279 return preparedManifest{}, fmt.Errorf("%w: P's files manifest %s is not a readable version", hostrelocate.ErrRefused, key)
280 }
281 version = v.VersionID
282 }
283 if version == "" {
284 return preparedManifest{}, fmt.Errorf("%w: P's files manifest %s has no current version", hostrelocate.ErrRefused, key)
285 }
286 raw, err := data.GetVersion(ctx, bucket, key, version)
287 if err != nil {
288 return preparedManifest{}, err
289 }
290 var m hostfiles.Manifest
291 if err := json.Unmarshal(raw, &m); err != nil || m.V != hostfiles.ManifestVersion || m.Seq != 0 || loaded.Seq != 0 || !sameEntries(m, loaded) {
292 return preparedManifest{}, errors.Join(fmt.Errorf("%w: P's files manifest %s@%s is not the prepared manifest the install judged", hostrelocate.ErrRefused, key, version), err)
293 }
294 sum := hostfilesSum(raw)
295 if sum != auth.FilesManifestSHA256 {
296 return preparedManifest{}, fmt.Errorf("%w: P's files manifest %s@%s is not the prepared authority's", hostrelocate.ErrRefused, key, version)
297 }
298 return preparedManifest{Key: key, SHA256: sum, Version: version, Op: op, Stamp: auth.InstallStamp}, nil
299}
300
301// relocationOpDir is the operation's local journal directory (ruling 44):
302// keyed by the op id, never by the host, so no region HA file under a
303// managed root names a host and the empty-installation census never sees
304// one (N9 makes relocate/ opaque).
305func relocationOpDir(dataDir, op string) (string, error) {
306 if dataDir == "" || !validOpID(op) {
307 return "", ErrClaimStopUnavailable
308 }
309 return filepath.Join(dataDir, "relocate", "ops", op), nil
310}
311
312func relocationInstallPath(dataDir, op string) (string, error) {
313 dir, err := relocationOpDir(dataDir, op)
314 if err != nil {
315 return "", err
316 }
317 return filepath.Join(dir, "install.json"), nil
318}
319
320// relocationRecord reads the host's authoritative record and the route
321// this box mints for it (ruling 45): always the nested layout, which the
322// isolated launch requires, so only a hibernating, path-deriving instance
323// record relocates. The route carries the app's update policy as the
324// ordinary mint's does (settings spec §3.2, provision.Reconcile): an
325// absent manifest is no policy, and a manifest that cannot be read
326// refuses, so a relocated instance never lands as auto when its app's
327// default is manual.
328func relocationRecord(ctx context.Context, dataDir string, claims release.Store, o hostclaim.Observed) (provision.ClaimRecord, error) {
329 if o.Claim == nil || o.Claim.Holder == nil {
330 return provision.ClaimRecord{}, hostbox.ErrChanged
331 }
332 rec, err := hostops.ReadRecord(ctx, claims, o.Claim.Holder.Key)
333 if err != nil {
334 return provision.ClaimRecord{}, err
335 }
336 var record provision.Record
337 if err := json.Unmarshal(rec.Body, &record); err != nil {
338 return provision.ClaimRecord{}, err
339 }
340 if record.Host != o.Host || !record.DerivesPaths() || !record.HibernateOrDefault() || record.Unit != "" {
341 return provision.ClaimRecord{}, fmt.Errorf("%w: %s is not a relocatable hibernating instance record", hostrelocate.ErrRefused, o.Host)
342 }
343 policy, err := relocationUpdatePolicy(ctx, claims, record)
344 if err != nil {
345 return provision.ClaimRecord{}, err
346 }
347 rt := provision.RouteFor(record, dataDir, "", true, policy)
348 if rt.DBPath != provision.NestedDBPath(dataDir, o.Host) || rt.Socket != provision.NestedSocket(o.Host) {
349 return provision.ClaimRecord{}, fmt.Errorf("%w: %s does not mint nested", hostrelocate.ErrRefused, o.Host)
350 }
351 return provision.ClaimRecord{Host: o.Host, Key: provision.RecordKey(record.Account, record.App, record.Host), Family: "instance", Account: record.Account, App: record.App, CreatedAt: record.CreatedAt, Value: record, Route: rt}, nil
352}
353
354// relocationUpdatePolicy is the app's update policy for record, read the
355// way provision.Reconcile's policyFor reads it: an absent manifest is
356// known to have none; any other read failure is an error, never "none".
357func relocationUpdatePolicy(ctx context.Context, store release.Store, record provision.Record) (*provision.UpdatePolicy, error) {
358 m, err := provision.GetManifest(ctx, store, record.Account, record.App)
359 switch {
360 case errors.Is(err, os.ErrNotExist):
361 return nil, nil
362 case err != nil:
363 return nil, fmt.Errorf("the update policy of %s/%s could not be read: %w", record.Account, record.App, err)
364 }
365 return m.Updates, nil
366}
367
368// relocationPin reads the pin the claim's op journals, through ReadPin.
369func relocationPin(ctx context.Context, claims release.Store, c *hostclaim.Claim) (hostrelocate.PinObject, error) {
370 var ref hostclaim.RecoveryPin
371 if err := json.Unmarshal([]byte(c.Op.Journal["pin"]), &ref); err != nil {
372 return hostrelocate.PinObject{}, fmt.Errorf("%w: the op journals no readable pin: %w", hostrelocate.ErrRefused, err)
373 }
374 return hostrelocate.ReadPin(ctx, claims, ref)
375}
376
377// relocationPlatformItem reports whether P's manifest m carries the
378// platform item name (the env file, the wake state) for the install to
379// restore, or the host has none (ruling 68, amended in Task 8c: absence is
380// never evidence). A live item is the host's. A manifest without it is
381// "none" only when the pin evidences a host that never published files:
382// a files pin with neither a digest nor a retained copy, which the pinner
383// admits only for a holder no writer was ever admitted to or a release
384// flush that published nothing, and which ReadPin refuses half-made (H2).
385// No app ran there, so there is no env file and no timer. Any other
386// manifest without the item refuses: platform items have no tombstone
387// (StagePlatformDelete drops the item), so a source whose env file was
388// deleted reads the same as one whose env file was never published, and a
389// manifest no release flush wrote (a failover's) carries no wake state
390// however many timers the host had. A tombstone, which no code writes,
391// refuses too.
392func relocationPlatformItem(pin hostrelocate.PinObject, m hostfiles.Manifest, name string) (bool, error) {
393 e, ok := m.Platform[name]
394 switch {
395 case ok && !e.Deleted:
396 return true, nil
397 case ok:
398 return false, fmt.Errorf("%w: P's manifest holds a tombstone for %s, which no publisher writes", hostrelocate.ErrRefused, name)
399 case pin.Files.SHA256 == "" && pin.Files.Retained == "":
400 return false, nil
401 default:
402 return false, fmt.Errorf("%w: P's manifest has no %s, and the pin does not evidence a host that never published files", hostrelocate.ErrRefused, name)
403 }
404}
405
406// relocationPolicy is region HA's relocating owner: it runs after the
407// claim's own gate and only refuses. The captured installation must be the
408// holder's, this box a participant and not lost.
409func relocationPolicy(box string) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
410 return func(_ context.Context, c *hostclaim.Claim, s hostbox.Selection, _ hostwrite.Identity) error {
411 if c == nil || c.Holder == nil || s.Box != box || !slices.Contains(c.Op.Participants, box) || slices.Contains(c.Lost, box) {
412 return hostbox.ErrChanged
413 }
414 if s.Captured.Incarnation != c.Holder.Incarnation || s.Captured.Stamp != c.Holder.Stamp {
415 return hostbox.ErrChanged
416 }
417 return nil
418 }
419}
420
421// relocationInstallPolicy is the typed install authority's policy (ruling
422// 46): only the three install actions, only under
423// relocationInstallAuthority at installed or probing, and a registry hold
424// absent or this op's; then region HA's relocating owner, which checks the
425// captured installation and can only refuse.
426func relocationInstallPolicy(held func(context.Context, string) (string, error), owner func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
427 return func(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
428 switch s.Action {
429 case relocationActionInstall, relocationActionHeldConfig, relocationActionHeldRelease:
430 default:
431 return hostbox.ErrChanged
432 }
433 if held == nil || owner == nil {
434 return hostbox.ErrUnsupported
435 }
436 if err := relocationInstallAuthority(c, s.Op, s.Box, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing); err != nil {
437 return errors.Join(hostbox.ErrChanged, err)
438 }
439 hold, err := held(ctx, s.Host)
440 if err != nil {
441 return err
442 }
443 if hold != "" && hold != c.Op.ID {
444 return hostbox.ErrChanged
445 }
446 return owner(ctx, c, s, id)
447 }
448}
449
450// relocationInstallSource is main's WorkSource with only its Policy
451// replaced (ruling 46): Guard, Box, Region, Now, Prepare (main's
452// participation) and Local (identity and quarantine, the lease's deadline
453// and loss latch, failed cleanup) are the assembly's own, and every With
454// re-checks them. Continue is nil: a phase change ends the selection.
455func relocationInstallSource(src hostbox.WorkSource, reg *registry.Registry) hostbox.WorkSource {
456 src.Policy = relocationInstallPolicy(hostbox.RegistryHoldPolicy(reg), relocationPolicy(src.Box))
457 src.Continue = nil
458 return src
459}
460
461// relocationInstall is §5.8's local install (B1-2) in ruling 44's order,
462// every write through the typed install authority (ruling 46):
463//
464// 1. the captured installation must be the active install stamp
465// (activated at installing by the build, or here for a replacement
466// that resumes at installed, relocationActivateInstalled; a retired
467// stamp is never reactivated, ruling 52);
468// 2. the claim's typed inventory says nested (park.Local, once);
469// 3. the build's scratch is removed (ruling 44);
470// 4. in the install work: MintHeld first (the census initialises the
471// installation from an empty box; the route entry; the row held by
472// the op with status relocating); then the instance directory through
473// main's PrepareState (ruling 57); then the files and env from P's
474// manifest into the nested paths, and the wake state;
475// 5. the held config (held-config selection), and the pinned key checked
476// against the env the target will launch with (O3');
477// 6. the pinned release (held-release selection; a blocked stage leaves
478// root's relay request, M3);
479// 7. the journal, then the receipt.
480//
481// Idempotent: a pass that ends in ErrNotYet is repeated whole.
482func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, error) {
483 activate := a.relocationActivateInstalled(cfg)
484 render, materialize, verifyLive := cfg.render, cfg.materialize, cfg.verifyLive
485 if verifyLive == nil {
486 verifyLive = release.VerifyHeldLiveUnder
487 }
488 if render == nil {
489 render = appconfig.RenderHeldUnder
490 }
491 if materialize == nil {
492 materialize = func(ctx context.Context, s release.Store, optDir string, rt registry.Route, version string, proof *release.ProofGate, work hostwrite.Work, guard func(func() error) error) (*release.Manifest, error) {
493 if hostwrite.Enabled(os.Getenv) {
494 return nil, errHeldReleaseUnavailable
495 }
496 return release.MaterializeHeldUnder(ctx, s, cfg.ReleasePub, cfg.Ratchet, optDir, rt, version, proof, work, guard)
497 }
498 }
499 return func(ctx context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, error) {
500 // Every precondition before anything is written (a misconfigured
501 // assembly leaves no row, file or claim write).
502 if cfg.Data == nil || cfg.Registry == nil || cfg.OptDir == "" || cfg.DataDir == "" || a.Reconcile == nil || a.Wake == nil || a.Wake.Roles == nil ||
503 cfg.Source.Box == "" || cfg.Source.Local == nil || o.Claim == nil || o.Claim.Holder == nil {
504 return hostclaim.LocalInstall{}, ErrClaimStopUnavailable
505 }
506 c := o.Claim
507 local, err := a.localStopIdentity(ctx)
508 if err != nil {
509 return hostclaim.LocalInstall{}, err
510 }
511 box := local.Actor.Box
512 check := func(fresh *hostclaim.Claim) error {
513 return relocationInstallAuthority(fresh, c.Op.ID, box, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing)
514 }
515 if err := check(c); err != nil {
516 return hostclaim.LocalInstall{}, err
517 }
518 stamp, _ := relocationInstallStamp(c)
519 pin, err := relocationPin(ctx, a.Claims, c)
520 if err != nil {
521 return hostclaim.LocalInstall{}, err
522 }
523 p := c.Op.Journal["prepared"]
524 prefix := moves.GenerationFilesPrefix(c.Holder.Location, p)
525 origins := append(append([]string(nil), pin.Files.Origins...), prefix)
526 // What P's manifest says about the env file and the wake state is
527 // judged before anything is written: a manifest missing either,
528 // under a pin that does not evidence its absence, refuses here with
529 // no row, file or claim write (absence is never evidence).
530 pm, found, err := hostfiles.Load(ctx, cfg.Data, prefix)
531 if err != nil {
532 return hostclaim.LocalInstall{}, err
533 }
534 if !found {
535 return hostclaim.LocalInstall{}, fmt.Errorf("%w: P's files manifest %s is missing", hostrelocate.ErrRefused, prefix)
536 }
537 for _, item := range []string{hostfiles.EnvItem, hostfiles.WakeItem} {
538 if _, err := relocationPlatformItem(pin, pm, item); err != nil {
539 return hostclaim.LocalInstall{}, err
540 }
541 }
542 // The binding the probe's C8 establishment reads P's prepared
543 // manifest by: its exact version, journalled below.
544 auth, err := preparedAuthority(c)
545 if err != nil {
546 return hostclaim.LocalInstall{}, err
547 }
548 binding, err := relocationPreparedBinding(ctx, cfg.Versions, cfg.Bucket, prefix, c.Op.ID, auth, pm)
549 if err != nil {
550 return hostclaim.LocalInstall{}, err
551 }
552 bindingRaw, err := json.Marshal(binding)
553 if err != nil {
554 return hostclaim.LocalInstall{}, err
555 }
556 rec, err := relocationRecord(ctx, cfg.DataDir, a.Claims, o)
557 if err != nil {
558 return hostclaim.LocalInstall{}, err
559 }
560 rt := rec.Route
561 paths := relocationPathsFor(cfg.DataDir, rt)
562 // The installation at the install stamp, activated here when this
563 // box resumes at installed without a build of its own (a
564 // replacement; the original target's build activated it already):
565 // a stamp the guard retired, or one older than its history, is
566 // never reactivated, and refuses.
567 captured, err := activate(ctx, o)
568 if errors.Is(err, hostguard.ErrRetiredStamp) || errors.Is(err, hostguard.ErrOldStamp) || errors.Is(err, hostguard.ErrStampMismatch) || errors.Is(err, hostguard.ErrNotParticipant) {
569 err = errors.Join(hostrelocate.ErrRefused, err)
570 }
571 if err != nil {
572 return hostclaim.LocalInstall{}, err
573 }
574 if captured.Stamp != stamp || captured.Incarnation != c.Holder.Incarnation {
575 return hostclaim.LocalInstall{}, fmt.Errorf("%w: the install stamp %d is not active here (%w)", hostrelocate.ErrRefused, stamp, hostguard.ErrStampMismatch)
576 }
577 src := relocationInstallSource(cfg.Source, cfg.Registry)
578 work, err := src.SelectAt(ctx, o.Host, relocationActionInstall, captured)
579 if err != nil {
580 return hostclaim.LocalInstall{}, err
581 }
582 // The claim's typed inventory for this box, once: RecordInventory
583 // writes the claim on every call, so a repeated install (a retry, a
584 // repairing EnsureLocal) does not rewrite it once it says nested
585 // (Astra round 2 finding 2; the participant re-reads after).
586 if entry, ok := c.Inventory[box]; !ok || entry.Layout != "nested" {
587 if err := a.Reconcile.Reconciler.Local.RecordInventory(ctx, o.Host, captured, "nested", nil, check); err != nil {
588 return hostclaim.LocalInstall{}, err
589 }
590 }
591 // The build's scratch names the host: gone before the census runs
592 // (ruling 44), even after a crash between the build and here.
593 if err := removeRelocationScratch(cfg, c.Op.ID); err != nil {
594 return hostclaim.LocalInstall{}, err
595 }
596 journal := relocationInstallJournal{V: 1, Host: o.Host, Op: c.Op.ID, Box: box, Enrollment: local.Actor.Enrollment, InstallStamp: stamp, Prepared: p, Steps: map[string]string{relocationStepFilesManifest: string(bindingRaw)}}
597 if err := a.Reconcile.MintHeldUnder(ctx, rec, captured, "relocating", c.Op.ID, work); err != nil {
598 return hostclaim.LocalInstall{}, err
599 }
600 journal.Steps["row"] = c.Op.ID
601 // The instance directory through main's typed protocol (ruling 57):
602 // ClaimRoleAssembly.PrepareState records the path entry intended,
603 // makes the directory, then records its device and inode
604 // materialized, under the install work; the probe's own
605 // PrepareState then finds it as its own. Never MkdirAll.
606 installed, err := hostclaim.Read(ctx, a.Claims, o.Host)
607 if err != nil {
608 return hostclaim.LocalInstall{}, err
609 }
610 if err := check(installed.Claim); err != nil {
611 return hostclaim.LocalInstall{}, err
612 }
613 if err := a.Wake.Roles.PrepareState(ctx, installed, work, ClaimLaunchInputs{DBPath: rt.DBPath}); err != nil {
614 return hostclaim.LocalInstall{}, err
615 }
616 var m hostfiles.Manifest
617 if err := work.With(ctx, hostwrite.Identity{}, func() error {
618 // Every write below lands in the instance directory, which the
619 // tenant can write (an install repeated after the app ran there
620 // meets what it left). So nothing here resolves a path the
621 // tenant could have linked: the files directory is made with
622 // mkdirat beneath the instance directory (EnsureDirBeneath, which
623 // refuses a link or a FIFO at the name), opened once with no
624 // symlink at any component (OpenRootBeneath) and restored from
625 // that root (RestoreRoot); the env file is written and removed
626 // beneath the directory (WriteAtomic, RemoveBeneath).
627 instDir, filesName := filepath.Dir(paths.FilesDir), filepath.Base(paths.FilesDir)
628 envDir, envName := filepath.Dir(paths.EnvPath), filepath.Base(paths.EnvPath)
629 if _, err := instancefs.EnsureDirBeneath(instDir, filesName, 0o700); err != nil {
630 return err
631 }
632 tenantRootSeam("checked")
633 root, err := instancefs.OpenRootBeneath(instDir, filesName)
634 if err != nil {
635 return err
636 }
637 defer root.Close()
638 tenantRootSeam("opened")
639 if m, _, err = hostfiles.RestoreRoot(ctx, cfg.Data, prefix, root, origins); err != nil {
640 // The root is named by its /proc/self/fd link, closed by
641 // the time anyone reads this: name the files directory.
642 return fmt.Errorf("hostfiles: restore into %s: %w", paths.FilesDir, err)
643 }
644 journal.Steps["files"] = strconv.FormatInt(m.Seq, 10)
645 // O3: the env file is P's EnvItem byte for byte; nothing is
646 // minted. No env file only by the pin's evidence (ruling 68 as
647 // amended, relocationPlatformItem): then any env file here is
648 // not the host's, so it goes. A manifest without the item and
649 // without that evidence refuses.
650 wantEnv, err := relocationPlatformItem(pin, m, hostfiles.EnvItem)
651 if err != nil {
652 return err
653 }
654 if wantEnv {
655 env, err := hostfiles.ReadPlatform(ctx, cfg.Data, prefix, m, hostfiles.EnvItem, origins)
656 if err != nil {
657 return err
658 }
659 // WriteAtomic fsyncs the file before its rename; the
660 // directory sync makes the rename durable.
661 if err := instancefs.WriteAtomic(envDir, envName, env, 0o600); err != nil {
662 return err
663 }
664 if err := syncTenantDir(envDir); err != nil {
665 return err
666 }
667 journal.Steps["env"] = "written"
668 } else {
669 if err := instancefs.RemoveBeneath(envDir, envName); err != nil && !errors.Is(err, os.ErrNotExist) {
670 return err
671 }
672 if err := syncTenantDir(envDir); err != nil {
673 return err
674 }
675 journal.Steps["env"] = "none"
676 }
677 // The wake state likewise: no timers only by the pin's evidence.
678 // A manifest no release flush wrote (a failover's) has none,
679 // and that is not a host without timers.
680 wantWake, err := relocationPlatformItem(pin, m, hostfiles.WakeItem)
681 if err != nil {
682 return err
683 }
684 journal.Steps["wake"] = "none"
685 if wantWake {
686 ws, _, err := hostfiles.LoadWake(ctx, cfg.Data, prefix, m, origins)
687 if err != nil {
688 return err
689 }
690 if err := hostfiles.ImportWake(cfg.Registry, o.Host, ws); err != nil {
691 return err
692 }
693 journal.Steps["wake"] = "imported"
694 }
695 return nil
696 }); err != nil {
697 return hostclaim.LocalInstall{}, err
698 }
699 held := rt
700 held.Status, held.HoldOp = "relocating", c.Op.ID
701 rendered, err := render(ctx, a.Claims, cfg.SecretsKey, cfg.DataDir, held, c.Op.ID, box, func(pre appconfig.HeldRender) (hostwrite.Work, error) {
702 w, err := src.SelectAt(ctx, o.Host, relocationActionHeldConfig, captured)
703 if err != nil {
704 return nil, err
705 }
706 return heldIdentityWork{Work: w, ID: HeldConfigIdentity(pre.BundleETags, pre.OverlayETag)}, nil
707 })
708 if err != nil {
709 return hostclaim.LocalInstall{}, err
710 }
711 journal.Steps["render"] = rendered.SHA256
712 // O3' (instance-key plan rev 3; ruling 55): when the pin carries the
713 // key by an adoption record, the env files this install will launch
714 // with must deliver exactly that key; a configuration changed since
715 // the pin refuses here, before anything launches. The adoption item
716 // itself is never restored: the target's sessions are new writers.
717 if pin.InstanceKey != nil {
718 // Both files are in the instance directory, which the tenant can
719 // write (an install repeated after the app has run there reads
720 // what it left), so both are read through main's one safe read
721 // for that ground (readInstanceFile), never os.ReadFile. The render
722 // always writes its file: a missing one is an error, as any
723 // other failure to read it is.
724 configEnv, absent, err := readInstanceFile(filepath.Dir(rendered.Path), filepath.Base(rendered.Path), instancekey.EnvReadLimit)
725 if err == nil && absent {
726 err = fmt.Errorf("%s: %w", rendered.Path, os.ErrNotExist)
727 }
728 if err != nil {
729 return hostclaim.LocalInstall{}, err
730 }
731 // The env file the install wrote must still be there: a missing
732 // one is not an env with no key of its own. Only an install
733 // that evidenced no env file reads its absence as none, and
734 // only when the directory itself opened (positive evidence).
735 ownEnv, absent, err := readInstanceFile(filepath.Dir(paths.EnvPath), filepath.Base(paths.EnvPath), instancekey.EnvReadLimit)
736 if err == nil && absent && journal.Steps["env"] != "none" {
737 err = fmt.Errorf("%s: %w", paths.EnvPath, os.ErrNotExist)
738 }
739 if err != nil {
740 return hostclaim.LocalInstall{}, errors.Join(hostrelocate.ErrRefused, err)
741 }
742 if err := instancekey.VerifyDelivered(configEnv, ownEnv, pin.InstanceKey.SHA256); err != nil {
743 return hostclaim.LocalInstall{}, errors.Join(hostrelocate.ErrRefused, err)
744 }
745 journal.Steps["key"] = pin.InstanceKey.SHA256
746 }
747 // The pinned release (R9): install_release, never the route's
748 // version, through the claim-aware held stage (never MaterializeHeld,
749 // which refuses with CARLOS_HOST_CLAIM on). The edge unit cannot
750 // write the version store: ErrStageBlocked leaves root's relay
751 // request (M3) and the receipt waits.
752 //
753 // As moves' T7 (heldReleaseReady): the publication holds the host's
754 // held-stage lock (withHeldStageLock) across its swap, re-hash and
755 // rollback, as root's relay does, and the receipt waits for the
756 // reading under that same lock (release.VerifyHeldLiveUnder: the
757 // link names the version and D3 accepts the bytes it names). A link
758 // alone is never evidence: a publication may be about to roll it
759 // back, and the bytes may have changed since.
760 version := c.Op.Journal["install_release"]
761 if version == "" {
762 return hostclaim.LocalInstall{}, fmt.Errorf("%w: no install_release journaled", hostrelocate.ErrRefused)
763 }
764 released, err := src.SelectAt(ctx, o.Host, relocationActionHeldRelease, captured)
765 if err != nil {
766 return hostclaim.LocalInstall{}, err
767 }
768 proof := release.NewRootProofGate(release.ProofPolicyPath, cfg.DataDir)
769 lock := func(fn func() error) error { return withHeldStageLock(cfg.DataDir, o.Host, fn) }
770 err = verifyLive(ctx, a.Claims, cfg.OptDir, held, version, proof, lock)
771 if errors.Is(err, release.ErrHeldNotLive) {
772 _, err = materialize(ctx, a.Claims, cfg.OptDir, held, version, proof, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: version}}, lock)
773 switch {
774 case errors.Is(err, release.ErrStageBlocked):
775 req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: o.Host, Version: version, Op: c.Op.ID, Box: box}
776 if err := writeHeldStageRequest(cfg.DataDir, req); err != nil {
777 return hostclaim.LocalInstall{}, err
778 }
779 return hostclaim.LocalInstall{}, fmt.Errorf("%w: the pinned release %s waits for root's held stage", hostrelocate.ErrNotYet, version)
780 case err != nil:
781 return hostclaim.LocalInstall{}, err
782 }
783 // Published by this call: the receipt waits for the same reading.
784 err = verifyLive(ctx, a.Claims, cfg.OptDir, held, version, proof, lock)
785 }
786 if errors.Is(err, release.ErrHeldNotLive) {
787 return hostclaim.LocalInstall{}, errors.Join(fmt.Errorf("%w: the pinned release %s is not live", hostrelocate.ErrNotYet, version), err)
788 }
789 if err != nil {
790 return hostclaim.LocalInstall{}, err
791 }
792 journal.Steps["materialize"] = version
793 raw, err := json.Marshal(journal)
794 if err != nil {
795 return hostclaim.LocalInstall{}, err
796 }
797 path, err := relocationInstallPath(cfg.DataDir, c.Op.ID)
798 if err != nil {
799 return hostclaim.LocalInstall{}, err
800 }
801 if err := work.With(ctx, hostwrite.Identity{}, func() error {
802 return writeRelocationJournal(path, raw)
803 }); err != nil {
804 return hostclaim.LocalInstall{}, err
805 }
806 sum := sha256.Sum256(raw)
807 return hostclaim.LocalInstall{Box: box, Enrollment: local.Actor.Enrollment, InstallStamp: stamp, FilesManifestVersion: m.Seq, JournalSHA256: hex.EncodeToString(sum[:])}, nil
808 }
809}
810
811// relocationVerifyLocal reports whether the receipt's install is whole on
812// this box now: the journal at the receipt's digest, the guard record
813// active at its stamp, the row held by the operation, and every manifest
814// entry and the env file as P's manifest says (hostfiles.Present), no env
815// file only by the pin's evidence (relocationPlatformItem).
816func (a ClaimAssembly) relocationVerifyLocal(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed, hostclaim.LocalInstall) (bool, error) {
817 receipt := a.relocationReceipt(cfg)
818 return func(ctx context.Context, o hostclaim.Observed, l hostclaim.LocalInstall) (bool, error) {
819 if cfg.Registry == nil || cfg.Data == nil || o.Claim == nil || o.Claim.Holder == nil {
820 return false, ErrClaimStopUnavailable
821 }
822 got, ok, err := receipt(ctx, o)
823 if err != nil || !ok || got != l {
824 return false, err
825 }
826 captured, err := hostguard.Capture(a.Guard.DataDir, o.Host)
827 if errors.Is(err, hostguard.ErrNoRecord) {
828 return false, nil
829 }
830 if err != nil {
831 return false, err
832 }
833 if captured.Stamp != l.InstallStamp {
834 return false, nil
835 }
836 row, err := cfg.Registry.Get(o.Host)
837 if err != nil {
838 return false, err
839 }
840 if row == nil || row.HoldOp != o.Claim.Op.ID || !registry.IsHold(row.Status) {
841 return false, nil
842 }
843 p := o.Claim.Op.Journal["prepared"]
844 prefix := moves.GenerationFilesPrefix(o.Claim.Holder.Location, p)
845 m, found, err := hostfiles.Load(ctx, cfg.Data, prefix)
846 if err != nil {
847 return false, err
848 }
849 if !found {
850 // Not "the install is incomplete": the install itself refuses
851 // without it, so a repair could not mend this (absence is
852 // never evidence).
853 return false, fmt.Errorf("%w: P's files manifest %s is missing", hostrelocate.ErrRefused, prefix)
854 }
855 // No env file only by the pin's evidence, as the install judged it.
856 pin, err := relocationPin(ctx, a.Claims, o.Claim)
857 if err != nil {
858 return false, err
859 }
860 wantEnv, err := relocationPlatformItem(pin, m, hostfiles.EnvItem)
861 if err != nil {
862 return false, err
863 }
864 paths := relocationPathsFor(cfg.DataDir, *row)
865 return hostfiles.Present(paths.FilesDir, paths.EnvPath, m, !wantEnv)
866 }
867}
868
869// relocationReceipt reads this box's durable install journal for the
870// claim's operation; false, with no error, only when there is none. A
871// journal that does not decode, names another host or operation, or
872// records no materialized release is an error, never "no receipt"
873// (final review minor 1, as readRelocationProbeFence): it is never
874// reinstalled over, so an operator keeps the evidence.
875func (a ClaimAssembly) relocationReceipt(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
876 return func(_ context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, bool, error) {
877 if o.Claim == nil {
878 return hostclaim.LocalInstall{}, false, ErrClaimStopUnavailable
879 }
880 path, err := relocationInstallPath(cfg.DataDir, o.Claim.Op.ID)
881 if err != nil {
882 return hostclaim.LocalInstall{}, false, err
883 }
884 raw, err := os.ReadFile(path)
885 if errors.Is(err, os.ErrNotExist) {
886 return hostclaim.LocalInstall{}, false, nil
887 }
888 if err != nil {
889 return hostclaim.LocalInstall{}, false, err
890 }
891 var j relocationInstallJournal
892 if err := json.Unmarshal(raw, &j); err != nil {
893 return hostclaim.LocalInstall{}, false, fmt.Errorf("%w: %s cannot be read: %v", hostrelocate.ErrRefused, path, err)
894 }
895 if j.V != 1 || j.Host != o.Host || j.Op != o.Claim.Op.ID || j.Steps["materialize"] == "" {
896 return hostclaim.LocalInstall{}, false, fmt.Errorf("%w: %s is not this operation's completed install journal", hostrelocate.ErrRefused, path)
897 }
898 seq, _ := strconv.ParseInt(j.Steps["files"], 10, 64)
899 sum := sha256.Sum256(raw)
900 return hostclaim.LocalInstall{Box: j.Box, Enrollment: j.Enrollment, InstallStamp: j.InstallStamp, FilesManifestVersion: seq, JournalSHA256: hex.EncodeToString(sum[:])}, true, nil
901 }
902}
903