carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// prepare.go is everything a nested role needs before its unit starts
2// (isolated-exec design §6.1): the launch file's directories, the merged
3// environment, the control listener (run only) and the role's log
4// listener. It is the nested counterpart of what startChild/seedChild do
5// inline for a flat host.
6package activator
7
8import (
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "log"
14 "net/http"
15 "os"
16 "path/filepath"
17 "strings"
18 "time"
19
20 "github.com/carlosframework/platform/internal/edge"
21 "github.com/carlosframework/platform/internal/instancectl"
22 "github.com/carlosframework/platform/internal/instancefs"
23 "github.com/carlosframework/platform/internal/instancekey"
24 "github.com/carlosframework/platform/internal/instanceupdates"
25 "github.com/carlosframework/platform/internal/provision"
26 "github.com/carlosframework/platform/internal/registry"
27 "github.com/carlosframework/platform/internal/release"
28 "github.com/carlosframework/platform/internal/schedule"
29)
30
31// prepare is everything a nested role needs before its unit starts
32// (design §6.1): the two directories, the admin token and merged env (for
33// run and seed; a cmd's env is its generation's, and prepare returns none), the control listener (run only), the role's log listener,
34// and stale records cleared. It returns the listeners so the caller can
35// hand them to the proc or close them on a failed start.
36func (b *ExecBackend) prepare(rt registry.Route, role instancectl.Role) (instancePaths, *edge.ControlListener, *logListener, map[string]string, error) {
37 return b.prepareAt(rt, role, rt.Socket, 0)
38}
39
40// prepareAt is prepare for a run role whose app socket is sock: rt.Socket,
41// or a generation socket (StartGen, zero-downtime spec §7.1). A generation
42// socket's own directory, g<n> in the run dir, is made first (rev 19, rule
43// 3). Only the file at sock is removed, never a sibling generation's, and
44// the control listener binds sock + ".control". The seed and sidecar roles
45// ignore sock.
46// gen is the generation unit being prepared (0 for the original templates):
47// it keys the log socket and the exit records cleared (zero-downtime spec
48// §7.6).
49func (b *ExecBackend) prepareAt(rt registry.Route, role instancectl.Role, sock string, gen uint64) (instancePaths, *edge.ControlListener, *logListener, map[string]string, error) {
50 p, ctl, logs, env, _, err := b.prepareKeyedAt(rt, role, sock, gen, nil)
51 return p, ctl, logs, env, err
52}
53
54// prepareKeyedAt is prepareAt returning, with the env, the layer its
55// CARLOS_INSTANCE_KEY came from, decided by the same read (instance-key
56// ruling 29). The nested run spawn keeps both on the generation's handle,
57// for a run's command against that generation (StartCmd).
58// steps, when not nil, times prepare's parts for the caller's journal line.
59func (b *ExecBackend) prepareKeyedAt(rt registry.Route, role instancectl.Role, sock string, gen uint64, steps *wakeSteps) (instancePaths, *edge.ControlListener, *logListener, map[string]string, instancekey.Layer, error) {
60 if steps == nil {
61 steps = &wakeSteps{}
62 } else {
63 steps.begin(time.Now())
64 }
65 p := b.pathsFor(rt)
66 if err := os.MkdirAll(p.dataDir, 0o700); err != nil {
67 return p, nil, nil, nil, instancekey.LayerNone, err
68 }
69 if err := os.MkdirAll(p.runDir, 0o750|os.ModeSetgid); err != nil {
70 return p, nil, nil, nil, instancekey.LayerNone, err
71 }
72 if err := os.MkdirAll(filepath.Dir(b.logSocketFor(rt.Host, role, gen)), 0o700); err != nil {
73 return p, nil, nil, nil, instancekey.LayerNone, err
74 }
75 steps.mark(time.Now(), "dirs")
76 var env map[string]string
77 var keyLayer instancekey.Layer // the layer env's key came from, read with it (ruling 29)
78 switch role {
79 case instancectl.RoleRun, instancectl.RoleSeed:
80 // p.dataDir is the instance directory here, tenant-writable ground
81 // (design §3.2), so the token file is minted and read through
82 // instancefs rather than a plain path-based open.
83 if err := EnsureAdminTokenBeneath(p.dataDir, rt.Host+".env"); err != nil {
84 return p, nil, nil, nil, instancekey.LayerNone, fmt.Errorf("admin token for %s: %w", rt.Host, err)
85 }
86 steps.mark(time.Now(), "admin token")
87 if _, err := b.ensureInstanceKeyFor(context.Background(), rt, p); err != nil { // prepareAt has no ctx to bound the mint's bucket read; configReadTimeout does
88 return p, nil, nil, nil, instancekey.LayerNone, fmt.Errorf("instance key for %s: %w", rt.Host, err)
89 }
90 steps.mark(time.Now(), "instance key")
91 var err error
92 env, keyLayer, err = b.instanceEnvLayer(rt)
93 if err != nil {
94 return p, nil, nil, nil, instancekey.LayerNone, err
95 }
96 steps.mark(time.Now(), "env")
97 case instancectl.RoleCmd:
98 // A run's environment is the one its generation launched with
99 // (merge ruling M3), which StartCmd has. The delivered files are
100 // not read: config sync may have written a revision the serving
101 // generation has not activated.
102 default:
103 var err error
104 env, err = readEnvFileBeneath(p.dataDir, filepath.Base(p.sidecarEnv))
105 if err != nil {
106 return p, nil, nil, nil, instancekey.LayerNone, err
107 }
108 keyLayer = sidecarKeyLayer(env)
109 }
110 // Stale exit records from earlier invocations of this role: a start
111 // that reads one would mis-word the very first exit line this
112 // invocation eventually gets, attributing a previous run's status to
113 // this one.
114 if entries, err := os.ReadDir(p.runDir); err == nil {
115 for _, e := range entries {
116 if strings.HasPrefix(e.Name(), exitPrefix(role, gen)) {
117 _ = os.Remove(filepath.Join(p.runDir, e.Name()))
118 }
119 }
120 }
121 var ctl *edge.ControlListener
122 if role == instancectl.RoleRun {
123 // Nothing is running: either this is the first invocation of this
124 // role for this host, or the unit is not active (Start would not
125 // have reached prepare otherwise), so any file at rt.Socket is a
126 // leftover from an earlier invocation, most likely one that was
127 // SIGKILLed before its own bind was ever established. On the
128 // nested path the agent itself never binds this socket (the app
129 // does, inside its own namespace, once the trampoline execs it),
130 // so unlike startChild's flat spawn there is no "our own live
131 // process squatting on it" case this could ever race: removing it
132 // here is safe by construction, and leaving it in place would fail
133 // EVERY later wake's health check with nothing but a dial timeout,
134 // no member-facing diagnosis at all, unlike the flat path's #129
135 // handling, which at least turns the bind failure into a named
136 // error (memberSafeWakeReason).
137 //
138 // A generation's socket lives in a directory of its own (rev 19,
139 // rule 1), made here with the run dir's mode. It inherits the run
140 // dir's setgid carlos-sockets group, and it sits inside the run dir
141 // the generation unit binds, so the app (uid carlos, as this agent
142 // is) can bind its socket there.
143 if dir := filepath.Dir(sock); dir != p.runDir {
144 if err := os.MkdirAll(dir, 0o750|os.ModeSetgid); err != nil {
145 return p, nil, nil, nil, instancekey.LayerNone, err
146 }
147 }
148 _ = os.Remove(sock)
149 ctl = b.listenControl(rt, sock+".control", p.envFile, gen)
150 steps.mark(time.Now(), "control socket")
151 }
152 // A replica's roles are gated like a flat launch (ruling 26): the seed
153 // and the sidecar have no control socket, the run role has the one just
154 // bound or none. The sidecar's key, if its own file holds one, is
155 // checked as the flat sidecar's is (sidecarKeyLayer). A cmd is gated in
156 // StartCmd, on the environment its generation launched with: prepare
157 // read none for it.
158 if role != instancectl.RoleCmd {
159 if err := b.replicaKeyGate(rt, env, keyLayer, ctl != nil); err != nil {
160 if ctl != nil {
161 ctl.Close()
162 }
163 return p, nil, nil, nil, instancekey.LayerNone, err
164 }
165 }
166 steps.mark(time.Now(), "key gate")
167 // The cmd role's output goes only to its own chunks, never to the
168 // journal or the app log, so it has no app-log listener.
169 var logs *logListener
170 if role != instancectl.RoleCmd {
171 var err error
172 logs, err = listenLogs(b.logSocketFor(rt.Host, role, gen), b.logEmitter(rt))
173 if err != nil {
174 if ctl != nil {
175 ctl.Close()
176 }
177 return p, nil, nil, nil, instancekey.LayerNone, fmt.Errorf("log listener for %s %s: %w", rt.Host, role, err)
178 }
179 }
180 steps.mark(time.Now(), "logs")
181 return p, ctl, logs, env, keyLayer, nil
182}
183
184// exitPrefix is the exit records prepare clears before a start: this
185// role's for the original templates, this generation's alone otherwise.
186// A sibling generation's record is never touched (zero-downtime spec
187// §7.6).
188func exitPrefix(role instancectl.Role, gen uint64) string {
189 if gen == 0 {
190 return "exit." + string(role) + "."
191 }
192 return instancectl.GenExitPrefix(gen)
193}
194
195// listenControl binds the control socket exactly as startChild does for an
196// exec child (the tokenReader and schedule mount are unchanged, aside from
197// reading beneath the instance directory rather than following a plain
198// path, design §3.2); a bind failure is logged and the instance runs
199// without its control verbs, as today.
200//
201// gen is the generation the listener serves (0 for the original
202// templates): its schedule writes go through that generation's write fence
203// (writeFence, final review F8).
204func (b *ExecBackend) listenControl(rt registry.Route, control, envPath string, gen uint64) *edge.ControlListener {
205 tokenReader := func() (string, error) {
206 env, err := readEnvFileBeneath(filepath.Dir(envPath), filepath.Base(envPath))
207 if err != nil {
208 return "", err
209 }
210 return env["CARLOS_ADMIN_TOKEN"], nil
211 }
212 var mount func(*http.ServeMux)
213 if b.Timers != nil || b.UpdatesReader != nil {
214 mount = func(mux *http.ServeMux) {
215 if b.Timers != nil {
216 schedule.MountGated(mux, rt.Host, b.Timers, tokenReader, time.Now, b.writeFence(rt.Host, gen))
217 }
218 if b.UpdatesReader != nil {
219 instanceupdates.Mount(mux, rt.Host, tokenReader, b.UpdatesReader)
220 }
221 }
222 }
223 ctl, err := edge.ListenControl(control, rt.Host, b.Cache, mount)
224 if err != nil {
225 log.Printf("activator: control listener for %s: %v (continuing without it)", rt.Host, err)
226 return nil
227 }
228 return ctl
229}
230
231// logSocket is logSocketFor for the original templates.
232func (b *ExecBackend) logSocket(host string, role instancectl.Role) string {
233 return b.logSocketFor(host, role, 0)
234}
235
236// logSocketFor is the role's log socket path, keyed by generation for a
237// generation unit, honouring the test override of the run directory
238// (RunDirOverride).
239func (b *ExecBackend) logSocketFor(host string, role instancectl.Role, gen uint64) string {
240 if gen != 0 {
241 if b.RunDirOverride != "" {
242 return filepath.Join(b.RunDirOverride, "logs", instancectl.GenKey(host, gen)+".run.sock")
243 }
244 return instancectl.GenLogSocket(host, gen)
245 }
246 return b.logSocketNamed(host, string(role))
247}
248
249// logSocketNamed is an original template's log socket for any listener
250// name: the cmd role has two, "cmd" and "cmd-err" (run spec §5.4).
251func (b *ExecBackend) logSocketNamed(host, name string) string {
252 if b.RunDirOverride != "" {
253 return filepath.Join(b.RunDirOverride, "logs", provision.InstanceID(host)+"."+name+".sock")
254 }
255 return provision.LogSocket(host, name)
256}
257
258// logEmitter is the same sink teeStd feeds, or nil when Logs is unset.
259func (b *ExecBackend) logEmitter(rt registry.Route) func(string) {
260 if b.Logs == nil {
261 return nil
262 }
263 return func(line string) { b.Logs.App(release.RouteAccount(rt), rt.App, rt.Host, line) }
264}
265
266// writeLaunch is writeLaunchFor for the original templates.
267func (b *ExecBackend) writeLaunch(p instancePaths, role instancectl.Role, host string, argv, env []string) error {
268 return b.writeLaunchFor(p, role, 0, host, argv, env)
269}
270
271// writeLaunchFor writes the launch file atomically into the instance
272// directory (design §6.2): launch.<role>.json, or launch.gen.<n>.json for a
273// generation (zero-downtime spec §7.6). The env is childEnv's deduplicated
274// list; the trampoline execs it as is.
275func (b *ExecBackend) writeLaunchFor(p instancePaths, role instancectl.Role, gen uint64, host string, argv, env []string) error {
276 raw, err := json.MarshalIndent(instancectl.Launch{Schema: instancectl.LaunchSchema, Host: host, Argv: argv, Env: env}, "", " ")
277 if err != nil {
278 return err
279 }
280 name := instancectl.LaunchFileName(role)
281 if gen != 0 {
282 name = instancectl.GenLaunchFileName(gen)
283 }
284 return instancefs.WriteAtomic(p.dataDir, name, append(raw, '\n'), 0o600)
285}
286
287// readEnvFileBeneath is readEnvFile through instancefs: a nested host's env
288// files are read without following a symlink (design §3.2). A missing file
289// is an empty env, as readEnvFile treats it (the caller's own
290// os.IsNotExist check would otherwise have nothing to check against).
291func readEnvFileBeneath(dir, name string) (map[string]string, error) {
292 b, err := instancefs.ReadBeneathLimit(dir, name, 1<<20)
293 if errors.Is(err, os.ErrNotExist) {
294 return map[string]string{}, nil
295 }
296 if err != nil {
297 return nil, err
298 }
299 return parseEnvBytes(b), nil
300}
301