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
3// The instance key (region HA request L21; rastrillo/rastrillo #66). An app
4// on CARLOS seals its data with a key. Before this file the app minted that
5// key itself after it started, so a box that died before the key reached a
6// backup left a database nothing could unseal. The platform now mints the
7// key on the box, into the instance's own <host>.env as
8// CARLOS_INSTANCE_KEY, before the app's first start, and never rewrites it.
9// rastrillo's carlos.InstanceKey reads it verbatim when the app has no key
10// of its own.
11//
12// The rules, each a ruling in
13// docs/superpowers/plans/2026-10-10-instance-key-provision.md:
14// - create-only: a key already in the file is never rewritten, whatever
15// its shape. A malformed one is reported and left in place, as
16// EnsureAdminToken does with a malformed token;
17// - the decision is made on the effective env (instancekey.Effective,
18// the rule mergeEnv applies): a key the rendered configuration
19// supplies is the app's key and nothing is minted; an empty own value
20// is no key;
21// - nothing is minted until the row's configuration is here. The files
22// say what this box has delivered, not what the configuration sets: a
23// first wake can land before the config sync's first render, and a
24// key minted then would shadow the configured one for good once it
25// arrived. So before it mints, the ensure asks the bucket
26// (ExecBackend.ConfiguredKey, the replica gate's reader) whether the
27// configuration sets a key now; if it does, or the bucket cannot say,
28// the launch is deferred (ErrInstanceKeyDeferred) and retried once the
29// sync has delivered the file;
30// - durable every time: before an ensure reports success the file is
31// 0600 (fchmod on its descriptor), the file is fsynced and so is its
32// directory, when the key was already there too. A sync that failed
33// once is redone by the next ensure, never skipped because the bytes
34// are already there. The known limit: once fsync has returned EIO,
35// Linux may mark the pages clean, so a later fsync's success does not
36// prove the bytes reached the disk. The retry narrows the gap; it
37// cannot close it, and no create-only design can;
38// - a mint that fails part way through its append is undone: the file
39// is truncated back to its length before the append and fsynced, so a
40// short key line never survives to be read as present. Those bytes
41// were never reported, so removing them rewrites no key;
42// - the value is never logged, never put in an error, and never leaves
43// the file except in the launch environment;
44// - a flat row's own file is opened O_NOFOLLOW on every path, read and
45// append alike (openOwnEnvNoFollow): a symlink there is refused, never
46// followed into a file a same-uid sibling chose. A nested row's file
47// gets the same from instancefs.
48
49import (
50 "context"
51 "crypto/rand"
52 "encoding/base64"
53 "errors"
54 "fmt"
55 "io"
56 "os"
57 "path/filepath"
58 "strings"
59 "time"
60
61 "github.com/carlosframework/platform/internal/instancefs"
62 "github.com/carlosframework/platform/internal/instancekey"
63 "github.com/carlosframework/platform/internal/registry"
64)
65
66// InstanceKeyVar is the variable the key is delivered as.
67const InstanceKeyVar = instancekey.Var
68
69// instanceKeyBytes is the key's length before encoding: 32 CSPRNG bytes,
70// standard base64 with padding, 44 characters. rastrillo uses the exact
71// bytes the env file yields, so this encoding is fixed for the life of
72// every key minted with it; changing it is a rotation.
73const instanceKeyBytes = 32
74
75// InstanceKeyOutcome is what one ensure did. Its zero value is
76// InstanceKeyFailed, what every ensure answers beside an error, so a
77// caller that reads the outcome before the error can never count a
78// refused row as anything.
79type InstanceKeyOutcome int
80
81const (
82 // InstanceKeyFailed: the ensure returned an error and reports nothing.
83 // It refused the row (fenced, conflicting, a configuration not yet
84 // delivered) or failed part way (an open, a write, a sync); the error
85 // says which. Nothing it may have written is reported as durable.
86 InstanceKeyFailed InstanceKeyOutcome = iota
87 // InstanceKeyPresent: the own env file already carried a key. No byte
88 // was written; the file was made durable.
89 InstanceKeyPresent
90 // InstanceKeyMinted: a key was appended and made durable.
91 InstanceKeyMinted
92 // InstanceKeyFromConfig: the rendered configuration supplies the key.
93 // Nothing was written.
94 InstanceKeyFromConfig
95 // InstanceKeyNoEnvFile: the backfill found no own env file, so the host
96 // has never been started here. Nothing was written; its first start
97 // mints the key.
98 InstanceKeyNoEnvFile
99 // InstanceKeyReplica: a replica row (#162). A replica is never minted
100 // a key: it may hold only its home's (ruling 26, replicaKeyGate).
101 InstanceKeyReplica
102)
103
104func (o InstanceKeyOutcome) String() string {
105 switch o {
106 case InstanceKeyFailed:
107 return "failed"
108 case InstanceKeyPresent:
109 return "present"
110 case InstanceKeyMinted:
111 return "minted"
112 case InstanceKeyFromConfig:
113 return "from-config"
114 case InstanceKeyNoEnvFile:
115 return "no-env-file"
116 case InstanceKeyReplica:
117 return "replica"
118 }
119 return fmt.Sprintf("outcome(%d)", int(o))
120}
121
122// instanceKeySync is the durability step's two syncs, a seam so a test can
123// fail each one. Read and replaced only under adminTokenMu.
124var instanceKeySync = struct {
125 file func(*os.File) error
126 dir func(string) error
127}{file: (*os.File).Sync, dir: syncDir}
128
129// syncDir fsyncs a directory, so an entry created in it survives a power
130// cut.
131func syncDir(dir string) error {
132 d, err := os.Open(dir)
133 if err != nil {
134 return err
135 }
136 err = d.Sync()
137 if cerr := d.Close(); err == nil {
138 err = cerr
139 }
140 return err
141}
142
143// instanceKeyWrite is the mint's append, a seam so a test can make it fail
144// part way (a short write, ENOSPC). Read and replaced only under
145// adminTokenMu.
146var instanceKeyWrite = func(f *os.File, b []byte) (int, error) { return f.Write(b) }
147
148// instanceKeyHooks are the three test seams on the launch path's hot path,
149// all nil in production; the sync and write seams above are what the
150// durability tests need and stay on their own. beforeOpen runs under
151// adminTokenMu after the read and the decision and before the ensure
152// opens the file to append or settle, so a test can change the file
153// between the two and pin the open's own symlink refusal, not only the
154// read's. betweenPhases runs after the ensure's first read (and its
155// bucket read, if any) and before it takes adminTokenMu, so a test can
156// change the files between the two phases as a concurrent writer can.
157// replicaGate runs at the start of every replica gate, after the env was
158// read, so a test can change the env files between the read and the
159// gate, as a concurrent config sync can.
160var instanceKeyHooks struct {
161 beforeOpen func(path string)
162 betweenPhases func(path string)
163 replicaGate func()
164}
165
166func mintInstanceKey() (string, error) {
167 raw := make([]byte, instanceKeyBytes)
168 if _, err := rand.Read(raw); err != nil {
169 return "", err
170 }
171 return base64.StdEncoding.EncodeToString(raw), nil
172}
173
174// wellFormedInstanceKey reports whether key has the shape this file mints.
175// A key that fails it is not necessarily broken (an operator may have
176// written one by hand), which is why a failure is a warning, never a
177// rotation.
178func wellFormedInstanceKey(key string) bool {
179 if len(key) != base64.StdEncoding.EncodedLen(instanceKeyBytes) {
180 return false
181 }
182 raw, err := base64.StdEncoding.Strict().DecodeString(key)
183 return err == nil && len(raw) == instanceKeyBytes
184}
185
186// configEnvName is the rendered configuration's file name beside an own env
187// file: <host>.env's sibling <host>.config.env, the pair pathsFor names.
188func configEnvName(envName string) string {
189 return strings.TrimSuffix(envName, ".env") + ".config.env"
190}
191
192// ErrInstanceKeyDeferred is a first start held back because the row's
193// configuration is not here yet: the bucket's configuration sets
194// CARLOS_INSTANCE_KEY and this box has not delivered it, or the bucket
195// could not be read to tell. Nothing is minted (ruling 5: a configured key
196// is the app's, and one minted while it was on its way would shadow it for
197// good). The launch fails and is retried: the config sync's next pass
198// delivers the file, and the next start finds the key configured.
199var ErrInstanceKeyDeferred = errors.New("activator: instance key not minted: the instance's configuration is not delivered")
200
201// configReadTimeout bounds each read of the bucket's configuration, by
202// the mint and by the replica gate.
203const configReadTimeout = 10 * time.Second
204
205// configuredKeyReader answers, for one row, the SHA-256 of the
206// CARLOS_INSTANCE_KEY the bucket's configuration renders now, "" for
207// none. nil is a box with no configuration source.
208type configuredKeyReader func(context.Context) (string, error)
209
210// configuredKeyFor is b.ConfiguredKey for rt, each read bounded by
211// configReadTimeout; nil when the backend has no reader.
212func (b *ExecBackend) configuredKeyFor(rt registry.Route) configuredKeyReader {
213 if b.ConfiguredKey == nil {
214 return nil
215 }
216 return func(ctx context.Context) (string, error) {
217 ctx, cancel := context.WithTimeout(ctx, configReadTimeout)
218 defer cancel()
219 return b.ConfiguredKey(ctx, rt)
220 }
221}
222
223// ensureInstanceKeyNoConfig makes sure path (an instance's own <host>.env
224// on the flat layout) carries CARLOS_INSTANCE_KEY, creating the file if
225// needed, for a row whose configuration can set no key: there is no
226// reader, so nothing is asked before the mint. Tests use it; every launch
227// path goes through ensureInstanceKeyFor, which asks the bucket first.
228func ensureInstanceKeyNoConfig(path string) (InstanceKeyOutcome, error) {
229 return ensureInstanceKey(context.Background(), filepath.Dir(path), filepath.Base(path), false, true, nil)
230}
231
232// ensureInstanceKeyBeneathNoConfig is ensureInstanceKeyNoConfig for a
233// nested row's own env file, read and written through instancefs because
234// the directory is tenant-writable (isolated-exec design §3.2).
235func ensureInstanceKeyBeneathNoConfig(dir, name string) (InstanceKeyOutcome, error) {
236 return ensureInstanceKey(context.Background(), dir, name, true, true, nil)
237}
238
239// ensureInstanceKeyFor is the launch paths' ensure for rt: none for a
240// replica row, the nested body for a nested row, the flat one otherwise.
241// ctx bounds the one bucket read a mint makes; a launch with none passes
242// context.Background().
243//
244// A fenced or conflicting row is refused and nothing is written, as
245// BackfillInstanceKey refuses it. Every launch dispatcher (Start,
246// StartGen, StartStaged, Seed) refuses those rows already, but on its own
247// classification: the launch body classifies again in pathsFor, and p is
248// that second answer. A layout journal written between the two (a
249// migration starting mid-wake) makes p fenced, with the flat directory as
250// its dataDir, and minting there would write to a half-migrated host
251// (isolated-exec design §8.2). The refusal is decided on p, the paths the
252// mint would use. Every caller names the host around the error ("instance
253// key for <host>: ..."), so the refusal does not.
254func (b *ExecBackend) ensureInstanceKeyFor(ctx context.Context, rt registry.Route, p instancePaths) (InstanceKeyOutcome, error) {
255 return b.ensureInstanceKeyAt(ctx, rt, p, true, "minted")
256}
257
258// BackfillInstanceKey is the startup backfill's step for one row: a key is
259// appended only to an own env file that already exists. A host with no file
260// has never been started on this box, and its first start mints the key;
261// creating a file here could also occupy the path a move's install expects
262// to be empty (moveslandinstall.go placeStaged). A row this backend would
263// refuse to start (fenced, conflict) is an error and nothing is written.
264// The agent's backfill names the host when it logs the error, so the
265// refusal does not.
266func (b *ExecBackend) BackfillInstanceKey(ctx context.Context, rt registry.Route) (InstanceKeyOutcome, error) {
267 return b.ensureInstanceKeyAt(ctx, rt, b.pathsFor(rt), false, "backfilled")
268}
269
270// ensureInstanceKeyAt is the two entry points' one guard: a replica row is
271// never minted; a fenced or conflicting row is refused on p, the paths the
272// mint would use, with verb naming which caller refused ("not minted",
273// "not backfilled"); every other row is ensureInstanceKey on p's layout,
274// with the bucket reader for rt.
275func (b *ExecBackend) ensureInstanceKeyAt(ctx context.Context, rt registry.Route, p instancePaths, create bool, verb string) (InstanceKeyOutcome, error) {
276 if rt.Replica {
277 return InstanceKeyReplica, nil
278 }
279 switch p.layout {
280 case LayoutFenced, LayoutConflict:
281 return InstanceKeyFailed, fmt.Errorf("not %s: %s", verb, p.reason)
282 }
283 return ensureInstanceKey(ctx, p.dataDir, rt.Host+".env", p.layout == LayoutNested, create, b.configuredKeyFor(rt))
284}
285
286// openOwnEnv opens an own env file, <host>.env under dir: through
287// instancefs when beneath (a nested row's tenant-writable directory), else
288// through openOwnEnvNoFollow. Both refuse a link or a FIFO in the open.
289// The one place the two layouts' opens meet.
290func openOwnEnv(dir, name string, beneath bool, flags int, perm os.FileMode) (*os.File, error) {
291 if beneath {
292 return instancefs.OpenBeneath(dir, name, flags, perm)
293 }
294 return openOwnEnvNoFollow(filepath.Join(dir, name), flags, perm)
295}
296
297// openInstanceEnv opens an own env file for append. Only a create passes
298// a mode: openat2 refuses a non-zero mode without O_CREAT (EINVAL), and an
299// existing file's mode is set by the fchmod that follows anyway.
300func openInstanceEnv(dir, name string, beneath, create bool) (*os.File, error) {
301 flags, perm := os.O_APPEND|os.O_WRONLY, os.FileMode(0)
302 if create {
303 flags, perm = flags|os.O_CREATE, 0o600
304 }
305 return openOwnEnv(dir, name, beneath, flags, perm)
306}
307
308// errOwnEnvSymlink is a flat row's own env file found to be a symlink. The
309// ensure refuses it on every path (mint, existing key, backfill, the claim
310// settle): following it would append to, chmod and fsync a file a same-uid
311// sibling chose (spec §7.2).
312var errOwnEnvSymlink = errors.New("activator: the instance env file is a symlink, refused")
313
314// errOwnEnvNotRegular is a flat row's own env file, or its rendered
315// configuration, found to be something other than a regular file: a FIFO,
316// which a blocking open would wait on for a writer that never comes (the
317// startup backfill holds adminTokenMu while it reads, so one planted FIFO
318// would keep every instance on the box from waking), or a device. Refused
319// on every path, as the link is.
320var errOwnEnvNotRegular = errors.New("activator: the instance env file is not a regular file, refused")
321
322// ownEnvReadLimit caps a flat read of an own env file or its rendered
323// configuration, as instancefs.ReadBeneathLimit caps the nested one: no
324// read under adminTokenMu is unbounded. It is instancekey's one cap, the
325// one relocation's reads of the same files use.
326const ownEnvReadLimit = instancekey.EnvReadLimit
327
328// errOwnEnvTooLarge is a flat env file over ownEnvReadLimit.
329var errOwnEnvTooLarge = errors.New("activator: the instance env file is larger than the limit, refused")
330
331// readOwnEnv reads a flat row's own env file, or its rendered
332// configuration, through openOwnEnvNoFollow, so the read refuses a link
333// or a FIFO exactly as the append does, and never reads more than
334// ownEnvReadLimit.
335func readOwnEnv(path string) ([]byte, error) {
336 f, err := openOwnEnvNoFollow(path, os.O_RDONLY, 0)
337 if err != nil {
338 return nil, err
339 }
340 defer f.Close()
341 raw, err := io.ReadAll(io.LimitReader(f, ownEnvReadLimit+1))
342 if err != nil {
343 return nil, err
344 }
345 if len(raw) > ownEnvReadLimit {
346 return nil, errOwnEnvTooLarge
347 }
348 return raw, nil
349}
350
351// settleInstanceEnv is the durability step every successful ensure ends
352// with: 0600 on the descriptor, then syncInstanceEnv. Caller holds
353// adminTokenMu.
354func settleInstanceEnv(f *os.File, dir, path string) error {
355 if err := f.Chmod(0o600); err != nil {
356 return fmt.Errorf("chmod %s: %w", path, err)
357 }
358 return syncInstanceEnv(f, dir, path)
359}
360
361// syncInstanceEnv fsyncs an own env file, then its directory: the two
362// syncs every durability step ends with, the claim's settle included.
363// Caller holds adminTokenMu.
364func syncInstanceEnv(f *os.File, dir, path string) error {
365 if err := instanceKeySync.file(f); err != nil {
366 return fmt.Errorf("sync %s: %w", path, err)
367 }
368 if err := instanceKeySync.dir(dir); err != nil {
369 return fmt.Errorf("sync %s: %w", dir, err)
370 }
371 return nil
372}
373
374// readInstanceEnvPair reads an own env file and its rendered configuration
375// the same guarded way on both layouts: through instancefs on the nested
376// one, and on the flat one through readOwnEnv, which refuses a link or a
377// FIFO at either name and caps the read. exists is false when the own file
378// is absent (raw nil); an absent configuration is nil cfg and no error.
379func readInstanceEnvPair(dir, name string, beneath bool) (raw, cfg []byte, exists bool, err error) {
380 read := func(n string) ([]byte, error) {
381 if beneath {
382 return instancefs.ReadBeneathLimit(dir, n, ownEnvReadLimit)
383 }
384 return readOwnEnv(filepath.Join(dir, n))
385 }
386 raw, err = read(name)
387 switch {
388 case errors.Is(err, os.ErrNotExist):
389 raw = nil
390 case err != nil:
391 return nil, nil, false, fmt.Errorf("read %s: %w", filepath.Join(dir, name), err)
392 default:
393 exists = true
394 }
395 cfg, err = read(configEnvName(name))
396 if err != nil && !errors.Is(err, os.ErrNotExist) {
397 return nil, nil, false, fmt.Errorf("read %s: %w", filepath.Join(dir, configEnvName(name)), err)
398 }
399 return raw, cfg, exists, nil
400}
401
402// bucketVerdict is what the bucket said before the lock was taken: asked
403// is whether a mint looked next on that read and the bucket was consulted;
404// deferral is its answer when the mint must wait (a configured key not yet
405// delivered, or a read that failed), nil when it may go ahead.
406type bucketVerdict struct {
407 asked bool
408 deferral error
409}
410
411// ensureInstanceKey is the shared body. It holds adminTokenMu, the lock
412// every mint into <host>.env takes, so a key append and a token append can
413// never interleave. create false is the backfill: an absent file is
414// InstanceKeyNoEnvFile and nothing is created.
415//
416// The bucket is read in two phases, so a slow or unreachable bucket for
417// one row never holds the lock every other row's mint waits on. Outside
418// the lock: read the files, and if a mint looks next, ask the bucket
419// (configured; the one read a row ever costs). Under the lock: read the
420// files again and decide from that read alone. A key that appeared in
421// either file since is present or configured, and nothing is minted; a
422// mint goes ahead only when the locked read still finds no key and the
423// bucket was asked and said none. A mint that looks next under the lock
424// without a verdict (the first read saw a key that has since gone) defers
425// rather than mint on nothing, and the next attempt asks. Create-only,
426// fail-closed and defer-on-unknown hold on every path.
427func ensureInstanceKey(ctx context.Context, dir, name string, beneath, create bool, configured configuredKeyReader) (InstanceKeyOutcome, error) {
428 path := filepath.Join(dir, name)
429 // Phase one, outside the lock. Nothing delivered here sets a key. The
430 // configuration may still: a first wake can run before the config
431 // sync's first render of <host>.config.env (the fast-converge pass
432 // reconciles the row ready and syncs config last), and a key minted
433 // in that gap would shadow the configured one for good once it
434 // arrived (Effective: the own file wins). So ask the bucket whether
435 // the configuration sets a key now. One it sets is not here yet:
436 // defer, and the next start finds it delivered. A bucket that cannot
437 // be read cannot say, and defers the same way. With no reader there
438 // is no configuration source, and nothing can be on its way. A read
439 // error here is left for the locked read to report.
440 var pre bucketVerdict
441 if configured != nil {
442 if raw, cfg, exists, err := readInstanceEnvPair(dir, name, beneath); err == nil && (exists || create) {
443 if _, layer := instancekey.Effective(parseEnvBytes(cfg), parseEnvBytes(raw)); layer == instancekey.LayerNone {
444 pre.asked = true
445 digest, err := configured(ctx)
446 switch {
447 case err != nil:
448 pre.deferral = fmt.Errorf("%w: its configuration could not be read: %w", ErrInstanceKeyDeferred, err)
449 case digest != "":
450 pre.deferral = fmt.Errorf("%w: its configuration sets %s and this box has not delivered it yet (the next config sync does, and the next start finds it)", ErrInstanceKeyDeferred, InstanceKeyVar)
451 }
452 }
453 }
454 }
455 if instanceKeyHooks.betweenPhases != nil {
456 instanceKeyHooks.betweenPhases(path)
457 }
458 // Phase two, under the lock: the decision, from this read only.
459 adminTokenMu.Lock()
460 defer adminTokenMu.Unlock()
461 raw, cfg, exists, err := readInstanceEnvPair(dir, name, beneath)
462 if err != nil {
463 return InstanceKeyFailed, err
464 }
465 if !exists && !create {
466 return InstanceKeyNoEnvFile, nil
467 }
468 // The decision is the effective env's: the key the launch will hand
469 // the app, by the same rule mergeEnv applies.
470 key, layer := instancekey.Effective(parseEnvBytes(cfg), parseEnvBytes(raw))
471 switch layer {
472 case instancekey.LayerConfig:
473 logPerFile("key-config", path, fmt.Sprintf("activator: %s is set by the app's configuration for %s: the platform mints none while it is", InstanceKeyVar, path))
474 return InstanceKeyFromConfig, nil
475 case instancekey.LayerOwn:
476 if !wellFormedInstanceKey(key) {
477 logPerFile("key-shape", path, fmt.Sprintf("activator: %s in %s is not the shape the platform mints (44 base64 characters, 32 bytes): left in place, because the app may already have sealed data with it", InstanceKeyVar, path))
478 }
479 // Present is not a no-op: a mint whose sync failed left these
480 // bytes behind, and this call is the retry that makes them
481 // durable before anything is started with them.
482 if instanceKeyHooks.beforeOpen != nil {
483 instanceKeyHooks.beforeOpen(path)
484 }
485 f, err := openInstanceEnv(dir, name, beneath, false)
486 if err != nil {
487 return InstanceKeyFailed, fmt.Errorf("open %s: %w", path, err)
488 }
489 defer f.Close()
490 if err := settleInstanceEnv(f, dir, path); err != nil {
491 return InstanceKeyFailed, err
492 }
493 return InstanceKeyPresent, nil
494 }
495 // A mint is next. It goes ahead only on the bucket's word from phase
496 // one; without it (the files changed between the two reads) nothing
497 // is known, and the mint defers rather than go on nothing.
498 if configured != nil {
499 switch {
500 case !pre.asked:
501 return InstanceKeyFailed, fmt.Errorf("%w: its env files changed while the configuration was being read (retried at the next start)", ErrInstanceKeyDeferred)
502 case pre.deferral != nil:
503 return InstanceKeyFailed, pre.deferral
504 }
505 }
506 minted, err := mintInstanceKey()
507 if err != nil {
508 return InstanceKeyFailed, fmt.Errorf("mint instance key for %s: %w", path, err)
509 }
510 if instanceKeyHooks.beforeOpen != nil {
511 instanceKeyHooks.beforeOpen(path)
512 }
513 f, err := openInstanceEnv(dir, name, beneath, create)
514 if errors.Is(err, os.ErrNotExist) && !create {
515 return InstanceKeyNoEnvFile, nil // removed since the read: nothing to backfill
516 }
517 if err != nil {
518 return InstanceKeyFailed, fmt.Errorf("open %s: %w", path, err)
519 }
520 defer f.Close()
521 // The length the append starts from, taken from the open descriptor:
522 // what a failed append is undone to. Under adminTokenMu it is
523 // len(raw); reading it here also keeps any byte another process
524 // appended since the read.
525 st, err := f.Stat()
526 if err != nil {
527 return InstanceKeyFailed, fmt.Errorf("stat %s: %w", path, err)
528 }
529 prefix := ""
530 if len(raw) > 0 && raw[len(raw)-1] != '\n' {
531 prefix = "\n"
532 }
533 line := []byte(prefix + InstanceKeyVar + "=" + minted + "\n")
534 if n, err := instanceKeyWrite(f, line); err != nil || n != len(line) {
535 if err == nil {
536 err = io.ErrShortWrite
537 }
538 return InstanceKeyFailed, undoInstanceKeyAppend(f, st.Size(), path, err)
539 }
540 if err := settleInstanceEnv(f, dir, path); err != nil {
541 return InstanceKeyFailed, err
542 }
543 return InstanceKeyMinted, nil
544}
545
546// undoInstanceKeyAppend removes a failed append's bytes: it truncates f
547// back to size, the length before the append, and fsyncs, so a short key
548// line can never survive to be read as present. Nothing past size was
549// ever reported to anyone. Returns the write error, joined with any
550// failure to undo it. Caller holds adminTokenMu.
551func undoInstanceKeyAppend(f *os.File, size int64, path string, werr error) error {
552 err := fmt.Errorf("write %s: %w", path, werr)
553 if terr := f.Truncate(size); terr != nil {
554 return errors.Join(err, fmt.Errorf("undo the partial write to %s: %w", path, terr))
555 }
556 if serr := instanceKeySync.file(f); serr != nil {
557 return errors.Join(err, fmt.Errorf("sync %s after undoing the partial write: %w", path, serr))
558 }
559 return err
560}
561
562// errInstanceEnvNeedsWrite is settleClaimInstanceEnv's answer for a file
563// whose mode is not 0600: making it so is a write, which the claim path
564// does only under its guard.
565var errInstanceEnvNeedsWrite = errors.New("activator: the instance env file needs its mode set")
566
567// settleClaimInstanceEnv is the claim launch source's durability step for
568// an env file that already carries what the launch needs: it makes the
569// file and its directory durable and writes nothing. fsync changes no
570// byte, mode or name, so it is not a host write and runs without the
571// claim's guard. A file that is not 0600 is errInstanceEnvNeedsWrite, and
572// the caller takes the guarded path, whose ensure sets the mode when the
573// own file holds the key (and leaves it when the key is configured or the
574// row is a replica: EnsureClaimAdminToken).
575func (b *ExecBackend) settleClaimInstanceEnv(rt registry.Route) error {
576 p := b.pathsFor(rt)
577 adminTokenMu.Lock()
578 defer adminTokenMu.Unlock()
579 f, err := openOwnEnv(p.dataDir, rt.Host+".env", p.layout == LayoutNested, os.O_RDONLY, 0)
580 if err != nil {
581 return fmt.Errorf("open %s: %w", p.envFile, err)
582 }
583 defer f.Close()
584 st, err := f.Stat()
585 if err != nil {
586 return fmt.Errorf("stat %s: %w", p.envFile, err)
587 }
588 if st.Mode().Perm() != 0o600 {
589 return errInstanceEnvNeedsWrite
590 }
591 return syncInstanceEnv(f, p.dataDir, p.envFile)
592}
593