carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "crypto/ed25519"
6 "fmt"
7 "os"
8
9 "github.com/carlosframework/platform/internal/registry"
10)
11
12// MaterializeHost stages host's channel head and points its live symlink —
13// the cold-wake, on-demand form of the adopt pass (carlos-deploy spec §5),
14// for an instance or a native sidecar (zero-downtime deploys spec §7.9).
15// It IS the adopt pass, filtered to one host with restart=nil (the host is
16// cold; the activator execing it is the "restart"): bake holds, signature
17// verification, staging, and the Version stamp are all the pass's own —
18// a second staging path is exactly the drift this refuses to create.
19//
20// Success is judged by the symlink, not the pass error: PassFilter logs
21// per-route trouble rather than returning it (only foundation failures — an
22// unreadable registry, an unwritable bake clock — come back as errors), and
23// "held by a bake window", "nothing promoted for this channel" and "no such
24// host" all correctly leave the link absent. A caller about to exec the
25// binary needs the link, so an absent link is this function's error.
26//
27// And judged by a link that RESOLVES, not merely one that exists. The caller
28// is ExecBackend.ensureLiveLink, which reaches this hook whenever os.Stat of
29// the link fails — and os.Stat FOLLOWS symlinks, so "link present, staged
30// version gone" (an operator's /opt cleanup, a partially restored volume)
31// arrives here exactly like "link absent". The pass cannot repair that state
32// on its own: with the channel head unchanged, adoptHost's cur == ver
33// short-circuit sees the dangling link's version via os.Readlink and returns
34// having staged nothing. So a dangling link is reported as the error it is,
35// naming the missing target — never as success the caller would answer by
36// exec'ing a path that does not resolve.
37//
38// dataDir is where the bake clock lives (BakeDir; /data in production), so
39// the window a cold wake observes is the same one the hourly root pass
40// measures — a cold wake must never become a bake bypass by keeping its own
41// clock.
42//
43// opts are appended to the internal pass's options — the caller's chance to
44// carry OnStageBlocked, so a cold wake on a hardened box relays its blocked
45// stage to the root pass instead of just erroring (the agent's
46// materializeHook does exactly this). BakeDir is always set first and is not
47// the caller's to override.
48func MaterializeHost(ctx context.Context, reg *registry.Registry, s Store, pub ed25519.PublicKey, optDir, dataDir, host string, opts ...PassOption) error {
49 only := func(rt registry.Route) bool { return rt.Host == host }
50 if err := PassFilter(ctx, reg, s, pub, optDir, nil, only, append([]PassOption{BakeDir(dataDir)}, opts...)...); err != nil {
51 return fmt.Errorf("materialize %s: %w", host, err)
52 }
53 link := LiveLink(optDir, host)
54 target, err := os.Readlink(link)
55 switch {
56 case err != nil && os.IsNotExist(err):
57 return fmt.Errorf("materialize %s: %s", host, noHeadReason(ctx, reg, s, pub, host))
58 case err != nil:
59 return fmt.Errorf("materialize %s: read live link %s: %w", host, link, err)
60 }
61 // The link is there; stat FOLLOWS it, so this is the question the caller
62 // actually has — is there a binary at the end of it?
63 if _, err := os.Stat(link); err != nil {
64 return fmt.Errorf("materialize %s: live link %s points at %s, which does not resolve (%v) — the staged version was removed; re-promote the channel or run `carlos adopt` on the box", host, link, target, err)
65 }
66 return nil
67}
68
69// noAdoptableHead is what an absent live link means when nothing more
70// specific can be PROVEN: the channel has no pointer, the pointer would not
71// verify, or its head is sitting out a bake window. All three leave the same
72// absence behind and all three are answered the same way — wait, or promote.
73const noAdoptableHead = "no adoptable head (nothing promoted for its channel, or the head is bake-held)"
74
75// StaticHeadUnderInstance is the marker internal/activator matches to render
76// the one absent-link cause that is neither "wait" nor "promote": a channel
77// whose head is a STATIC release, under a route that execs a binary.
78// Exported so the member-facing renderer and this diagnosis cannot drift into
79// matching on different words.
80const StaticHeadUnderInstance = "the head is a static release"
81
82// noHeadReason explains an absent live link, in the member's terms where it
83// can and in the generic terms above where it cannot.
84//
85// It exists for one case the generic sentence gets actively WRONG. A static
86// release's manifest keys its artifact "site", not "<goos>-<goarch>", so
87// ensureStaged refuses to stage it ("no linux-arm64 build — skipping"), the
88// pass logs that box-side and moves on, and the link is never written. The
89// member is then told nothing is promoted for the channel while `carlos
90// channels` shows the promote that plainly did happen — two contradictory
91// true statements, and no way from either to the actual problem, which is
92// that an instance route can never serve a static site (skills#29).
93//
94// The route class is checked, not assumed: only a Kind=="instance" route
95// execs a binary, and only for one is the mismatch this names real.
96//
97// Two bucket reads, on the failure path only. A wake that already failed can
98// afford them, and nothing on the success path pays anything. Every step
99// degrades to the generic reason — a diagnosis that cannot be made must not
100// become a second, worse error on top of the first.
101func noHeadReason(ctx context.Context, reg *registry.Registry, s Store, pub ed25519.PublicKey, host string) string {
102 rt, err := reg.Get(host)
103 if err != nil || rt == nil || rt.Kind != "instance" {
104 return noAdoptableHead
105 }
106 acct, channel := RouteAccount(*rt), routeChannel(*rt)
107 ptr, err := Channel(ctx, s, pub, acct, rt.App, channel)
108 if err != nil || ptr == nil || ptr.Version == "" {
109 return noAdoptableHead // genuinely nothing promoted, or a pointer that would not verify
110 }
111 m, err := GetManifest(ctx, s, acct, rt.App, ptr.Version)
112 if err != nil || m == nil || m.Kind != "static" {
113 return noAdoptableHead
114 }
115 return fmt.Sprintf("%s: %s %s is promoted on %s, but a static site is served straight off the "+
116 "channel pointer and has no binary to run, so this route can never wake into it. "+
117 "A static site needs a static route: `carlos instances delete --app %s --host %s`, then "+
118 "`carlos deploy --app %s --kind static --host %s <site-dir>`",
119 StaticHeadUnderInstance, rt.App, ptr.Version, channel, rt.App, host, rt.App, host)
120}
121