carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "path/filepath"
13
14 "github.com/carlosframework/platform/internal/cgroup"
15 "github.com/carlosframework/platform/internal/hostwrite"
16 "github.com/carlosframework/platform/internal/instancekey"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21// ClaimLaunchSnapshot is read-only launch selection. The actual launch still
22// opens and hashes the executable under the captured work immediately before exec.
23//
24// Two readers take one. SnapshotClaimLaunch is the pure read: the claim's
25// input policy compares its Identity to a Work's, and nothing is launched,
26// so no gate runs and no bucket is read. SelectClaimLaunch is the launch
27// sources' read: the same snapshot, then the replica gate on the env and
28// the layer that read produced (ruling 29), with its fresh bucket read for
29// a configured key (ruling 27).
30type ClaimLaunchSnapshot struct {
31 Identity hostwrite.Identity
32 Executable, SHA256, Version string
33 // Staged is the file SHA256 hashes, from one resolution of the live
34 // link (release.ResolveLiveLink): the version it is staged under, its path
35 // and that digest. Version is a separate read of the link; the claimed
36 // start refuses when the two disagree (ClaimStart.Staged).
37 Staged release.StagedIdentity
38 Args, Env []string
39 Roles []string
40 Limit cgroup.Limit
41 // InstanceKeySHA256 and InstanceKeySource name the CARLOS_INSTANCE_KEY
42 // Env delivers (its digest) and the file that supplied it
43 // (instancekey.SourcePlatform or SourceConfig); both empty when Env
44 // delivers none. O6 hands them to the process's adoption verb.
45 InstanceKeySHA256, InstanceKeySource string
46}
47
48// claimLaunchRoute is the route shape the claim launch path serves.
49func claimLaunchRoute(rt registry.Route) bool {
50 return rt.Host != "" && rt.Socket != "" && filepath.IsAbs(rt.DBPath) && !rt.IsSidecar() && rt.Unit == "" && rt.Backing == "" && rt.Tunnel == ""
51}
52
53// EnsureClaimAdminToken is the claim path's half of what startChild and
54// seedChild do before reading the env: a first wake has no
55// CARLOS_ADMIN_TOKEN and no CARLOS_INSTANCE_KEY until something mints
56// them, and the claim path never runs the legacy launch preparation. A
57// token already present is left exactly as it is, so a retry never rotates
58// it. Only a missing token, or a missing key, is minted, and only inside
59// authorize, which must run the write under the claim's current serving
60// authority and refuse otherwise. SnapshotClaimLaunch stays read-only.
61func (b *ExecBackend) EnsureClaimAdminToken(ctx context.Context, rt registry.Route, authorize func(context.Context, func() error) error) error {
62 if ctx.Err() != nil {
63 return ctx.Err()
64 }
65 if !claimLaunchRoute(rt) {
66 return hostwrite.ErrUnavailable
67 }
68 if err := b.claimLaunchShape(rt); err != nil {
69 return err
70 }
71 vars, err := b.instanceEnv(rt)
72 if err != nil {
73 return err
74 }
75 // The instance key is initialized the same way and in the same
76 // authorized write (L21). vars is the effective env, so a key the
77 // rendered configuration supplies counts as present, and a replica
78 // gets none (ruling 26). When both are present nothing is written and
79 // authorize is not called, but the file is still made durable before
80 // the launch source answers: a key whose mint failed its sync is never
81 // launched with until a sync succeeds (settleClaimInstanceEnv, ruling
82 // 18). A file that is not 0600 takes the guarded path. The key's
83 // ensure sets the mode when the own file holds the key; when the key
84 // is the configuration's, or the row is a replica, the ensure writes
85 // nothing, the mode stays as it is, and the next launch of that row
86 // asks the guard again. The token's mode has never been the claim
87 // path's to tighten.
88 if vars["CARLOS_ADMIN_TOKEN"] != "" && (rt.Replica || vars[InstanceKeyVar] != "") {
89 err := b.settleClaimInstanceEnv(rt)
90 switch {
91 case err == nil:
92 return nil
93 case errors.Is(err, os.ErrNotExist):
94 // No own env file: the token and the key both come from the
95 // rendered configuration. A configured key is the app's and
96 // is not the platform's to make durable (ruling 5), and an
97 // absent file is not created here (ruling 7), so the launch
98 // proceeds as it did before the key existed.
99 return nil
100 case !errors.Is(err, errInstanceEnvNeedsWrite):
101 return err
102 }
103 }
104 if authorize == nil {
105 return hostwrite.ErrUnavailable
106 }
107 paths := b.pathsFor(rt)
108 return authorize(ctx, func() error {
109 // Only a missing token is minted: one the rendered configuration
110 // supplies is the one the launch delivers, and a second one in the
111 // own file would shadow it.
112 if vars["CARLOS_ADMIN_TOKEN"] == "" {
113 if err := ensureAdminTokenAt(paths, rt.Host+".env"); err != nil {
114 return err
115 }
116 }
117 // The one place the claim launch source names the host around the
118 // key's error: the refusal itself does not, and every other launch
119 // path wraps it the same way.
120 if _, err := b.ensureInstanceKeyFor(ctx, rt, paths); err != nil {
121 return fmt.Errorf("instance key for %s: %w", rt.Host, err)
122 }
123 return nil
124 })
125}
126
127// SnapshotClaimLaunch is the read-only snapshot with no gate: what the
128// claim's input policy compares identities with. It reads the env files
129// and the live build; it never reads the bucket, and a replica row that
130// the launch would refuse still snapshots here, because the question is
131// "is the identity still this one", not "may this start".
132func (b *ExecBackend) SnapshotClaimLaunch(ctx context.Context, rt registry.Route) (ClaimLaunchSnapshot, error) {
133 out, _, _, err := b.snapshotClaimLaunch(ctx, rt)
134 return out, err
135}
136
137// SelectClaimLaunch is the launch sources' snapshot: SnapshotClaimLaunch,
138// then the replica gate on the env and the layer of that same read
139// (ruling 29). A claim launch has no control socket until O6, so a replica
140// is selected only with its home's key, verified (ruling 26); the gate's
141// fresh bucket read for a configured key (ruling 27) runs here and only
142// here on the claim path.
143func (b *ExecBackend) SelectClaimLaunch(ctx context.Context, rt registry.Route) (ClaimLaunchSnapshot, error) {
144 out, vars, layer, err := b.snapshotClaimLaunch(ctx, rt)
145 if err != nil {
146 return out, err
147 }
148 if err := b.replicaKeyGate(rt, vars, layer, false); err != nil {
149 return ClaimLaunchSnapshot{}, err
150 }
151 return out, nil
152}
153
154// snapshotClaimLaunch composes the snapshot and hands back, from the one
155// read that composed its env, the env and the layer its key came from, for
156// SelectClaimLaunch's gate.
157func (b *ExecBackend) snapshotClaimLaunch(ctx context.Context, rt registry.Route) (ClaimLaunchSnapshot, map[string]string, instancekey.Layer, error) {
158 var out ClaimLaunchSnapshot
159 if ctx.Err() != nil {
160 return out, nil, instancekey.LayerNone, ctx.Err()
161 }
162 if !claimLaunchRoute(rt) {
163 return out, nil, instancekey.LayerNone, hostwrite.ErrUnavailable
164 }
165 if err := b.claimLaunchShape(rt); err != nil {
166 return out, nil, instancekey.LayerNone, err
167 }
168 cfgEnv, ownEnv, err := b.instanceEnvParts(rt)
169 if err != nil {
170 return out, nil, instancekey.LayerNone, err
171 }
172 vars := mergeEnv(cfgEnv, ownEnv) // instanceEnv, with its two layers kept
173 if vars["CARLOS_ADMIN_TOKEN"] == "" {
174 return out, nil, instancekey.LayerNone, hostwrite.ErrUnavailable
175 }
176 // The key this launch delivers and the file that supplied it, from
177 // the same read as vars: what the replica gate decides on (ruling 29)
178 // and what the process's adoption report is checked against (ruling
179 // 21, O6). Not in the identity: the env already is.
180 key, layer := instancekey.Effective(cfgEnv, ownEnv)
181 if key != "" {
182 out.InstanceKeySHA256, out.InstanceKeySource = instancekey.Digest(key), instancekey.SourceOf(layer)
183 }
184 out.Executable = b.liveLink(rt.Host)
185 // One resolution of the live link names both the file hashed below and
186 // the version it is staged under (settings spec §4.2, ruling
187 // R-EXEC-CLAIMVERSION): opening the link itself would hash whatever it
188 // named at that moment, with no way to say which build that was.
189 out.Staged, err = release.ResolveLiveLink(b.OptDir, rt.Host)
190 if err != nil {
191 return out, nil, instancekey.LayerNone, err
192 }
193 f, err := os.Open(out.Staged.StagedPath)
194 if err != nil {
195 return out, nil, instancekey.LayerNone, err
196 }
197 info, err := f.Stat()
198 if err != nil || !info.Mode().IsRegular() {
199 f.Close()
200 return out, nil, instancekey.LayerNone, hostwrite.ErrUnavailable
201 }
202 digest := sha256.New()
203 _, err = io.Copy(digest, f)
204 closeErr := f.Close()
205 if err != nil {
206 return out, nil, instancekey.LayerNone, err
207 }
208 if closeErr != nil {
209 return out, nil, instancekey.LayerNone, closeErr
210 }
211 out.SHA256 = hex.EncodeToString(digest.Sum(nil))
212 out.Staged.Digest = out.SHA256
213 out.Version, err = b.LiveVersion(rt.Host)
214 if err != nil || out.Version == "" {
215 return out, nil, instancekey.LayerNone, hostwrite.ErrUnavailable
216 }
217 out.Roles, err = b.ClaimLaunchRoles(rt)
218 if err != nil {
219 return out, nil, instancekey.LayerNone, err
220 }
221 out.Args = []string{"--socket", rt.Socket, "--db", rt.DBPath}
222 out.Env = childEnv(b.agentOwnedVars(rt, "", vars, false))
223 raw, err := json.Marshal(out.Env)
224 if err != nil {
225 return out, nil, instancekey.LayerNone, err
226 }
227 config := sha256.Sum256(raw)
228 out.Identity = hostwrite.Identity{Config: hex.EncodeToString(config[:]), Release: out.SHA256}
229 out.Limit = b.tier(rt)
230 return out, vars, layer, nil
231}
232