activator: mint CARLOS_INSTANCE_KEY create-only into <host>.env, durable on every call (L21)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+666
−0
internal/activator/instancekey.go+329 −0internal/activator/instancekey_test.go+337 −0
diff --git a/internal/activator/instancekey.go b/internal/activator/instancekey.go| new file mode 100644 |
| index 000000000..2ab5ae99f |
| --- /dev/null |
| +++ b/internal/activator/instancekey.go |
| @@ -0,0 +1,329 @@ |
| +package activator |
| + |
| +// The instance key (region HA request L21; rastrillo/rastrillo #66). An app |
| +// on CARLOS seals its data with a key. Before this file the app minted that |
| +// key itself after it started, so a box that died before the key reached a |
| +// backup left a database nothing could unseal. The platform now mints the |
| +// key on the box, into the instance's own <host>.env as |
| +// CARLOS_INSTANCE_KEY, before the app's first start, and never rewrites it. |
| +// rastrillo's carlos.InstanceKey reads it verbatim when the app has no key |
| +// of its own. |
| +// |
| +// The rules, each a ruling in |
| +// docs/superpowers/plans/2026-10-10-instance-key-provision.md: |
| +// - create-only: a key already in the file is never rewritten, whatever |
| +// its shape. A malformed one is reported and left in place, as |
| +// EnsureAdminToken does with a malformed token; |
| +// - the decision is made on the effective env (instancekey.Effective, |
| +// the rule mergeEnv applies): a key the rendered configuration |
| +// supplies is the app's key and nothing is minted; an empty own value |
| +// is no key; |
| +// - durable every time: before an ensure reports success the file is |
| +// 0600 (fchmod on its descriptor), the file is fsynced and so is its |
| +// directory, when the key was already there too. A sync that failed |
| +// once is redone by the next ensure, never skipped because the bytes |
| +// are already there; |
| +// - the value is never logged, never put in an error, and never leaves |
| +// the file except in the launch environment. |
| + |
| +import ( |
| + "crypto/rand" |
| + "encoding/base64" |
| + "errors" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/instancefs" |
| + "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// InstanceKeyVar is the variable the key is delivered as. |
| +const InstanceKeyVar = instancekey.Var |
| + |
| +// instanceKeyBytes is the key's length before encoding: 32 CSPRNG bytes, |
| +// standard base64 with padding, 44 characters. rastrillo uses the exact |
| +// bytes the env file yields, so this encoding is fixed for the life of |
| +// every key minted with it; changing it is a rotation. |
| +const instanceKeyBytes = 32 |
| + |
| +// InstanceKeyOutcome is what one ensure did. |
| +type InstanceKeyOutcome int |
| + |
| +const ( |
| + // InstanceKeyPresent: the own env file already carried a key. No byte |
| + // was written; the file was made durable. |
| + InstanceKeyPresent InstanceKeyOutcome = iota |
| + // InstanceKeyMinted: a key was appended and made durable. |
| + InstanceKeyMinted |
| + // InstanceKeyFromConfig: the rendered configuration supplies the key. |
| + // Nothing was written. |
| + InstanceKeyFromConfig |
| + // InstanceKeyNoEnvFile: the backfill found no own env file, so the host |
| + // has never been started here. Nothing was written; its first start |
| + // mints the key. |
| + InstanceKeyNoEnvFile |
| + // InstanceKeyReplica: a replica row (#162). A replica is never minted |
| + // a key: it may hold only its home's (ruling 26, replicaKeyGate). |
| + InstanceKeyReplica |
| +) |
| + |
| +func (o InstanceKeyOutcome) String() string { |
| + switch o { |
| + case InstanceKeyPresent: |
| + return "present" |
| + case InstanceKeyMinted: |
| + return "minted" |
| + case InstanceKeyFromConfig: |
| + return "from-config" |
| + case InstanceKeyNoEnvFile: |
| + return "no-env-file" |
| + case InstanceKeyReplica: |
| + return "replica" |
| + } |
| + return fmt.Sprintf("outcome(%d)", int(o)) |
| +} |
| + |
| +// instanceKeySync is the durability step's two syncs, a seam so a test can |
| +// fail each one. Read and replaced only under adminTokenMu. |
| +var instanceKeySync = struct { |
| + file func(*os.File) error |
| + dir func(string) error |
| +}{file: (*os.File).Sync, dir: syncDir} |
| + |
| +// syncDir fsyncs a directory, so an entry created in it survives a power |
| +// cut. |
| +func syncDir(dir string) error { |
| + d, err := os.Open(dir) |
| + if err != nil { |
| + return err |
| + } |
| + err = d.Sync() |
| + if cerr := d.Close(); err == nil { |
| + err = cerr |
| + } |
| + return err |
| +} |
| + |
| +func mintInstanceKey() (string, error) { |
| + raw := make([]byte, instanceKeyBytes) |
| + if _, err := rand.Read(raw); err != nil { |
| + return "", err |
| + } |
| + return base64.StdEncoding.EncodeToString(raw), nil |
| +} |
| + |
| +// wellFormedInstanceKey reports whether key has the shape this file mints. |
| +// A key that fails it is not necessarily broken (an operator may have |
| +// written one by hand), which is why a failure is a warning, never a |
| +// rotation. |
| +func wellFormedInstanceKey(key string) bool { |
| + if len(key) != base64.StdEncoding.EncodedLen(instanceKeyBytes) { |
| + return false |
| + } |
| + raw, err := base64.StdEncoding.Strict().DecodeString(key) |
| + return err == nil && len(raw) == instanceKeyBytes |
| +} |
| + |
| +// configEnvName is the rendered configuration's file name beside an own env |
| +// file: <host>.env's sibling <host>.config.env, the pair pathsFor names. |
| +func configEnvName(envName string) string { |
| + return strings.TrimSuffix(envName, ".env") + ".config.env" |
| +} |
| + |
| +// EnsureInstanceKey makes sure path (an instance's own <host>.env on the |
| +// flat layout) carries CARLOS_INSTANCE_KEY, creating the file if needed. |
| +// Launch paths call it right after EnsureAdminToken. |
| +func EnsureInstanceKey(path string) (InstanceKeyOutcome, error) { |
| + return ensureInstanceKey(filepath.Dir(path), filepath.Base(path), false, true) |
| +} |
| + |
| +// EnsureInstanceKeyBeneath is EnsureInstanceKey for a nested row's own env |
| +// file, read and written through instancefs because the directory is |
| +// tenant-writable (isolated-exec design §3.2). |
| +func EnsureInstanceKeyBeneath(dir, name string) (InstanceKeyOutcome, error) { |
| + return ensureInstanceKey(dir, name, true, true) |
| +} |
| + |
| +// ensureInstanceKeyFor is the launch paths' ensure for rt: none for a |
| +// replica row, the nested body for a nested row, the flat one otherwise. |
| +func (b *ExecBackend) ensureInstanceKeyFor(rt registry.Route, p instancePaths) (InstanceKeyOutcome, error) { |
| + if rt.Replica { |
| + return InstanceKeyReplica, nil |
| + } |
| + if p.layout == LayoutNested { |
| + return EnsureInstanceKeyBeneath(p.dataDir, rt.Host+".env") |
| + } |
| + return EnsureInstanceKey(p.envFile) |
| +} |
| + |
| +// BackfillInstanceKey is the startup backfill's step for one row: a key is |
| +// appended only to an own env file that already exists. A host with no file |
| +// has never been started on this box, and its first start mints the key; |
| +// creating a file here could also occupy the path a move's install expects |
| +// to be empty (moveslandinstall.go placeStaged). A row this backend would |
| +// refuse to start (fenced, conflict) is an error and nothing is written. |
| +func (b *ExecBackend) BackfillInstanceKey(rt registry.Route) (InstanceKeyOutcome, error) { |
| + if rt.Replica { |
| + return InstanceKeyReplica, nil |
| + } |
| + p := b.pathsFor(rt) |
| + switch p.layout { |
| + case LayoutNested: |
| + return ensureInstanceKey(p.dataDir, rt.Host+".env", true, false) |
| + case LayoutFenced, LayoutConflict: |
| + return InstanceKeyPresent, fmt.Errorf("%s: not backfilled: %s", rt.Host, p.reason) |
| + } |
| + return ensureInstanceKey(p.dataDir, rt.Host+".env", false, false) |
| +} |
| + |
| +// openInstanceEnv opens an own env file for append. Only a create passes |
| +// a mode: openat2 refuses a non-zero mode without O_CREAT (EINVAL), and an |
| +// existing file's mode is set by the fchmod that follows anyway. |
| +func openInstanceEnv(dir, name string, beneath, create bool) (*os.File, error) { |
| + flags, perm := os.O_APPEND|os.O_WRONLY, os.FileMode(0) |
| + if create { |
| + flags, perm = flags|os.O_CREATE, 0o600 |
| + } |
| + if beneath { |
| + return instancefs.OpenBeneath(dir, name, flags, perm) |
| + } |
| + return os.OpenFile(filepath.Join(dir, name), flags, perm) |
| +} |
| + |
| +// settleInstanceEnv is the durability step every successful ensure ends |
| +// with: 0600 on the descriptor, then the file, then its directory. Caller |
| +// holds adminTokenMu. |
| +func settleInstanceEnv(f *os.File, dir, path string) error { |
| + if err := f.Chmod(0o600); err != nil { |
| + return fmt.Errorf("chmod %s: %w", path, err) |
| + } |
| + if err := instanceKeySync.file(f); err != nil { |
| + return fmt.Errorf("sync %s: %w", path, err) |
| + } |
| + if err := instanceKeySync.dir(dir); err != nil { |
| + return fmt.Errorf("sync %s: %w", dir, err) |
| + } |
| + return nil |
| +} |
| + |
| +// ensureInstanceKey is the shared body. It holds adminTokenMu, the lock |
| +// every mint into <host>.env takes, so a key append and a token append can |
| +// never interleave. create false is the backfill: an absent file is |
| +// InstanceKeyNoEnvFile and nothing is created. |
| +func ensureInstanceKey(dir, name string, beneath, create bool) (InstanceKeyOutcome, error) { |
| + path := filepath.Join(dir, name) |
| + adminTokenMu.Lock() |
| + defer adminTokenMu.Unlock() |
| + read := func(n string) ([]byte, error) { |
| + if beneath { |
| + return instancefs.ReadBeneathLimit(dir, n, 1<<20) |
| + } |
| + return os.ReadFile(filepath.Join(dir, n)) |
| + } |
| + raw, err := read(name) |
| + switch { |
| + case errors.Is(err, os.ErrNotExist): |
| + if !create { |
| + return InstanceKeyNoEnvFile, nil |
| + } |
| + case err != nil: |
| + return InstanceKeyPresent, fmt.Errorf("read %s: %w", path, err) |
| + } |
| + cfg, err := read(configEnvName(name)) |
| + if err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return InstanceKeyPresent, fmt.Errorf("read %s: %w", filepath.Join(dir, configEnvName(name)), err) |
| + } |
| + // The decision is the effective env's: the key the launch will hand |
| + // the app, by the same rule mergeEnv applies. |
| + key, layer := instancekey.Effective(parseEnvBytes(cfg), parseEnvBytes(raw)) |
| + switch layer { |
| + case instancekey.LayerConfig: |
| + 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)) |
| + return InstanceKeyFromConfig, nil |
| + case instancekey.LayerOwn: |
| + if !wellFormedInstanceKey(key) { |
| + 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)) |
| + } |
| + // Present is not a no-op: a mint whose sync failed left these |
| + // bytes behind, and this call is the retry that makes them |
| + // durable before anything is started with them. |
| + f, err := openInstanceEnv(dir, name, beneath, false) |
| + if err != nil { |
| + return InstanceKeyPresent, fmt.Errorf("open %s: %w", path, err) |
| + } |
| + defer f.Close() |
| + if err := settleInstanceEnv(f, dir, path); err != nil { |
| + return InstanceKeyPresent, err |
| + } |
| + return InstanceKeyPresent, nil |
| + } |
| + minted, err := mintInstanceKey() |
| + if err != nil { |
| + return InstanceKeyPresent, fmt.Errorf("mint instance key for %s: %w", path, err) |
| + } |
| + f, err := openInstanceEnv(dir, name, beneath, create) |
| + if errors.Is(err, os.ErrNotExist) && !create { |
| + return InstanceKeyNoEnvFile, nil // removed since the read: nothing to backfill |
| + } |
| + if err != nil { |
| + return InstanceKeyPresent, fmt.Errorf("open %s: %w", path, err) |
| + } |
| + defer f.Close() |
| + prefix := "" |
| + if len(raw) > 0 && raw[len(raw)-1] != '\n' { |
| + prefix = "\n" |
| + } |
| + if _, err := fmt.Fprintf(f, "%s%s=%s\n", prefix, InstanceKeyVar, minted); err != nil { |
| + return InstanceKeyPresent, fmt.Errorf("write %s: %w", path, err) |
| + } |
| + if err := settleInstanceEnv(f, dir, path); err != nil { |
| + return InstanceKeyPresent, err |
| + } |
| + return InstanceKeyMinted, nil |
| +} |
| + |
| +// errInstanceEnvNeedsWrite is settleClaimInstanceEnv's answer for a file |
| +// whose mode is not 0600: making it so is a write, which the claim path |
| +// does only under its guard. |
| +var errInstanceEnvNeedsWrite = errors.New("activator: the instance env file needs its mode set") |
| + |
| +// settleClaimInstanceEnv is the claim launch source's durability step for |
| +// an env file that already carries what the launch needs: it makes the |
| +// file and its directory durable and writes nothing. fsync changes no |
| +// byte, mode or name, so it is not a host write and runs without the |
| +// claim's guard. A file that is not 0600 is errInstanceEnvNeedsWrite, and |
| +// the caller takes the guarded path, whose ensure sets the mode. |
| +func (b *ExecBackend) settleClaimInstanceEnv(rt registry.Route) error { |
| + p := b.pathsFor(rt) |
| + name := rt.Host + ".env" |
| + adminTokenMu.Lock() |
| + defer adminTokenMu.Unlock() |
| + var f *os.File |
| + var err error |
| + if p.layout == LayoutNested { |
| + f, err = instancefs.OpenBeneath(p.dataDir, name, os.O_RDONLY, 0) |
| + } else { |
| + f, err = os.Open(p.envFile) |
| + } |
| + if err != nil { |
| + return fmt.Errorf("open %s: %w", p.envFile, err) |
| + } |
| + defer f.Close() |
| + st, err := f.Stat() |
| + if err != nil { |
| + return fmt.Errorf("stat %s: %w", p.envFile, err) |
| + } |
| + if st.Mode().Perm() != 0o600 { |
| + return errInstanceEnvNeedsWrite |
| + } |
| + if err := instanceKeySync.file(f); err != nil { |
| + return fmt.Errorf("sync %s: %w", p.envFile, err) |
| + } |
| + if err := instanceKeySync.dir(p.dataDir); err != nil { |
| + return fmt.Errorf("sync %s: %w", p.dataDir, err) |
| + } |
| + return nil |
| +} |
diff --git a/internal/activator/instancekey_test.go b/internal/activator/instancekey_test.go| new file mode 100644 |
| index 000000000..263d8a9dd |
| --- /dev/null |
| +++ b/internal/activator/instancekey_test.go |
| @@ -0,0 +1,337 @@ |
| +package activator |
| + |
| +import ( |
| + "bytes" |
| + "encoding/base64" |
| + "errors" |
| + "log" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "sync" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// keyLines returns every CARLOS_INSTANCE_KEY value in raw, in order. |
| +func keyLines(raw []byte) []string { |
| + var out []string |
| + for _, line := range strings.Split(string(raw), "\n") { |
| + if v, ok := strings.CutPrefix(line, InstanceKeyVar+"="); ok { |
| + out = append(out, v) |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// syncProbe counts the key's syncs and fails them on demand. Set the |
| +// error fields only between calls. |
| +type syncProbe struct { |
| + file, dir int |
| + fileErr, dirErr error |
| +} |
| + |
| +func probeInstanceKeySync(t *testing.T) *syncProbe { |
| + t.Helper() |
| + p := &syncProbe{} |
| + adminTokenMu.Lock() |
| + old := instanceKeySync |
| + instanceKeySync.file = func(f *os.File) error { |
| + p.file++ |
| + if p.fileErr != nil { |
| + return p.fileErr |
| + } |
| + return f.Sync() |
| + } |
| + instanceKeySync.dir = func(d string) error { |
| + p.dir++ |
| + if p.dirErr != nil { |
| + return p.dirErr |
| + } |
| + return syncDir(d) |
| + } |
| + adminTokenMu.Unlock() |
| + t.Cleanup(func() { |
| + adminTokenMu.Lock() |
| + instanceKeySync = old |
| + adminTokenMu.Unlock() |
| + }) |
| + return p |
| +} |
| + |
| +func TestEnsureInstanceKeyMintsOnceInTheAgreedFormat(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + got, err := EnsureInstanceKey(p) |
| + if err != nil || got != InstanceKeyMinted { |
| + t.Fatalf("first ensure = %v, %v; want minted", got, err) |
| + } |
| + raw, _ := os.ReadFile(p) |
| + keys := keyLines(raw) |
| + if len(keys) != 1 { |
| + t.Fatalf("file holds %d keys, want 1", len(keys)) |
| + } |
| + dec, err := base64.StdEncoding.Strict().DecodeString(keys[0]) |
| + if err != nil || len(dec) != 32 || len(keys[0]) != 44 { |
| + t.Fatalf("key is not 32 bytes in padded standard base64 (len %d, decode err %v)", len(keys[0]), err) |
| + } |
| + if !bytes.HasSuffix(raw, []byte("\n")) { |
| + t.Fatal("the key line has no newline: the next append would join it") |
| + } |
| + if st, _ := os.Stat(p); st.Mode().Perm() != 0o600 { |
| + t.Fatalf("mode = %o, want 600", st.Mode().Perm()) |
| + } |
| + // The parser every launch path uses yields the exact characters minted. |
| + env, _ := readEnvFile(p) |
| + if env[InstanceKeyVar] != keys[0] { |
| + t.Fatal("readEnvFile does not yield the minted key verbatim") |
| + } |
| + again, err := EnsureInstanceKey(p) |
| + if err != nil || again != InstanceKeyPresent { |
| + t.Fatalf("second ensure = %v, %v; want present", again, err) |
| + } |
| + raw2, _ := os.ReadFile(p) |
| + if !bytes.Equal(raw, raw2) { |
| + t.Fatal("second ensure changed the file") |
| + } |
| +} |
| + |
| +// Never overwrite: a key already there, of any shape, is kept byte for byte |
| +// and only reported. |
| +func TestEnsureInstanceKeyNeverRewritesAnExistingKey(t *testing.T) { |
| + for _, existing := range []string{ |
| + "CARLOS_INSTANCE_KEY=hand-written-not-base64\n", |
| + "A=1\nCARLOS_INSTANCE_KEY=" + strings.Repeat("A", 43) + "=", // well formed, no trailing newline |
| + } { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + if err := os.WriteFile(p, []byte(existing), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + var buf bytes.Buffer |
| + log.SetOutput(&buf) |
| + got, err := EnsureInstanceKey(p) |
| + log.SetOutput(os.Stderr) |
| + if err != nil || got != InstanceKeyPresent { |
| + t.Fatalf("ensure over %q = %v, %v; want present", existing, got, err) |
| + } |
| + if raw, _ := os.ReadFile(p); string(raw) != existing { |
| + t.Fatalf("file changed: %q -> %q", existing, raw) |
| + } |
| + if strings.Contains(buf.String(), "hand-written-not-base64") { |
| + t.Fatal("the shape warning logged the key's value") |
| + } |
| + } |
| +} |
| + |
| +func TestEnsureInstanceKeyAppendsOnAFreshLineAndKeepsEveryOtherLine(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + before := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("a", 64) + "\n# comment\nDATABASE_URL=sqlite" // no trailing newline |
| + if err := os.WriteFile(p, []byte(before), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := EnsureInstanceKey(p); err != nil || got != InstanceKeyMinted { |
| + t.Fatalf("ensure = %v, %v", got, err) |
| + } |
| + raw, _ := os.ReadFile(p) |
| + if !strings.HasPrefix(string(raw), before+"\n"+InstanceKeyVar+"=") { |
| + t.Fatalf("file = %q: the key must land on its own line after the untouched original", raw) |
| + } |
| + env, _ := readEnvFile(p) |
| + if env["DATABASE_URL"] != "sqlite" || len(env["CARLOS_ADMIN_TOKEN"]) != 64 { |
| + t.Fatal("an existing line was lost") |
| + } |
| + if st, _ := os.Stat(p); st.Mode().Perm() != 0o600 { |
| + t.Fatalf("mode = %o, want the 0644 file tightened to 600", st.Mode().Perm()) |
| + } |
| +} |
| + |
| +// A key the app's configuration supplies is the app's key: minting one into |
| +// <host>.env would override it (mergeEnv) and change the key under its data. |
| +// The decision is the effective env's, so an empty CARLOS_INSTANCE_KEY= in |
| +// the own file does not hide the configured key (finding 4). |
| +func TestEnsureInstanceKeyDefersToTheRenderedConfiguration(t *testing.T) { |
| + for _, own := range []string{"A=1\n", "A=1\n" + InstanceKeyVar + "=\n"} { |
| + dir := t.TempDir() |
| + p := filepath.Join(dir, "shop.example.com.env") |
| + if err := os.WriteFile(filepath.Join(dir, "shop.example.com.config.env"), []byte(InstanceKeyVar+"=from-config\n"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(p, []byte(own), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := EnsureInstanceKey(p) |
| + if err != nil || got != InstanceKeyFromConfig { |
| + t.Fatalf("own %q: ensure = %v, %v; want from-config", own, got, err) |
| + } |
| + if raw, _ := os.ReadFile(p); string(raw) != own { |
| + t.Fatalf("own file changed: %q", raw) |
| + } |
| + } |
| +} |
| + |
| +func TestBackfillInstanceKeyAppendsOnlyToAnExistingEnvFile(t *testing.T) { |
| + b := &ExecBackend{DataDir: t.TempDir()} |
| + fresh := registry.Route{Host: "new.example.com", Socket: "/run/carlos/new.sock"} |
| + got, err := b.BackfillInstanceKey(fresh) |
| + if err != nil || got != InstanceKeyNoEnvFile { |
| + t.Fatalf("backfill with no env file = %v, %v; want no-env-file", got, err) |
| + } |
| + if _, err := os.Lstat(filepath.Join(b.DataDir, fresh.Host+".env")); !os.IsNotExist(err) { |
| + t.Fatalf("backfill created an env file: %v", err) |
| + } |
| + old := registry.Route{Host: "shop.example.com", Socket: "/run/carlos/shop.sock"} |
| + envPath := filepath.Join(b.DataDir, old.Host+".env") |
| + tok := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("b", 64) + "\n" |
| + if err := os.WriteFile(envPath, []byte(tok), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := b.BackfillInstanceKey(old); err != nil || got != InstanceKeyMinted { |
| + t.Fatalf("backfill = %v, %v; want minted", got, err) |
| + } |
| + raw, _ := os.ReadFile(envPath) |
| + if !strings.HasPrefix(string(raw), tok) || len(keyLines(raw)) != 1 { |
| + t.Fatalf("file = %q", raw) |
| + } |
| + // Idempotent: a second backfill (the next agent start) writes nothing. |
| + if got, err := b.BackfillInstanceKey(old); err != nil || got != InstanceKeyPresent { |
| + t.Fatalf("second backfill = %v, %v; want present", got, err) |
| + } |
| + if raw2, _ := os.ReadFile(envPath); !bytes.Equal(raw, raw2) { |
| + t.Fatal("second backfill changed the file") |
| + } |
| +} |
| + |
| +// Review focus 1: a hand-edited file with two keys. Readers take the last |
| +// (parseEnvBytes), and the ensure writes nothing. |
| +func TestEnsureInstanceKeyLeavesAFileWithTwoKeysAlone(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + body := InstanceKeyVar + "=first\n" + InstanceKeyVar + "=second\n" |
| + if err := os.WriteFile(p, []byte(body), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := EnsureInstanceKey(p); err != nil || got != InstanceKeyPresent { |
| + t.Fatalf("ensure = %v, %v; want present", got, err) |
| + } |
| + if raw, _ := os.ReadFile(p); string(raw) != body { |
| + t.Fatalf("file changed: %q", raw) |
| + } |
| + if env, _ := readEnvFile(p); env[InstanceKeyVar] != "second" { |
| + t.Fatalf("readers see %q, want the last line's value", env[InstanceKeyVar]) |
| + } |
| +} |
| + |
| +// Review focus 2: an empty value is no key. One is appended after it, and |
| +// readers take the appended one. |
| +func TestEnsureInstanceKeyTreatsAnEmptyValueAsAbsent(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + if err := os.WriteFile(p, []byte(InstanceKeyVar+"=\n"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := EnsureInstanceKey(p); err != nil || got != InstanceKeyMinted { |
| + t.Fatalf("ensure = %v, %v; want minted", got, err) |
| + } |
| + env, _ := readEnvFile(p) |
| + if !wellFormedInstanceKey(env[InstanceKeyVar]) { |
| + t.Fatal("readers do not see the appended key") |
| + } |
| +} |
| + |
| +// Review focus 3: a seed, a visitor's wake and the backfill can ask at |
| +// once. Exactly one key is written. |
| +func TestEnsureInstanceKeyMintsOneKeyUnderConcurrentCallers(t *testing.T) { |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + var wg sync.WaitGroup |
| + for i := 0; i < 8; i++ { |
| + wg.Add(1) |
| + go func() { |
| + defer wg.Done() |
| + if _, err := EnsureInstanceKey(p); err != nil { |
| + t.Error(err) |
| + } |
| + }() |
| + } |
| + wg.Wait() |
| + if raw, _ := os.ReadFile(p); len(keyLines(raw)) != 1 { |
| + t.Fatalf("%d keys written", len(keyLines(raw))) |
| + } |
| +} |
| + |
| +// Review focus 5: an empty CARLOS_INSTANCE_KEY in the rendered |
| +// configuration supplies nothing, so the platform mints one. |
| +func TestEnsureInstanceKeyMintsWhenTheConfigurationValueIsEmpty(t *testing.T) { |
| + dir := t.TempDir() |
| + if err := os.WriteFile(filepath.Join(dir, "shop.example.com.config.env"), []byte(InstanceKeyVar+"=\n"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := EnsureInstanceKey(filepath.Join(dir, "shop.example.com.env")); err != nil || got != InstanceKeyMinted { |
| + t.Fatalf("ensure = %v, %v; want minted", got, err) |
| + } |
| +} |
| + |
| +// Finding 2: a mint whose file or directory sync fails reports failure, |
| +// and so does every retry until a sync succeeds. The retry finds the key |
| +// present, keeps its bytes, and still completes the chmod and both syncs |
| +// before it reports success. |
| +func TestEnsureInstanceKeyRedoesAFailedSyncBeforeReportingSuccess(t *testing.T) { |
| + for _, tc := range []struct { |
| + name string |
| + fail func(*syncProbe, error) |
| + }{ |
| + {"file sync", func(p *syncProbe, err error) { p.fileErr = err }}, |
| + {"directory sync", func(p *syncProbe, err error) { p.dirErr = err }}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + probe := probeInstanceKeySync(t) |
| + p := filepath.Join(t.TempDir(), "shop.example.com.env") |
| + tc.fail(probe, errors.New("injected: EIO")) |
| + if _, err := EnsureInstanceKey(p); err == nil { |
| + t.Fatal("a mint whose sync failed reported success") |
| + } |
| + raw, _ := os.ReadFile(p) |
| + if len(keyLines(raw)) != 1 { |
| + t.Fatalf("the failed mint left %d keys, want its 1", len(keyLines(raw))) |
| + } |
| + if _, err := EnsureInstanceKey(p); err == nil { |
| + t.Fatal("a retry reported success while the sync still fails") |
| + } |
| + if err := os.Chmod(p, 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + tc.fail(probe, nil) |
| + before := *probe |
| + got, err := EnsureInstanceKey(p) |
| + if err != nil || got != InstanceKeyPresent { |
| + t.Fatalf("retry = %v, %v; want present", got, err) |
| + } |
| + if probe.file != before.file+1 || probe.dir != before.dir+1 { |
| + t.Fatalf("the retry synced the file %d and the directory %d times, want once each", probe.file-before.file, probe.dir-before.dir) |
| + } |
| + if raw2, _ := os.ReadFile(p); !bytes.Equal(raw, raw2) { |
| + t.Fatal("a retry rewrote the key") |
| + } |
| + if st, _ := os.Stat(p); st.Mode().Perm() != 0o600 { |
| + t.Fatalf("mode = %o, want the present path to set 600", st.Mode().Perm()) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// Ruling 26: a replica row is never minted a key, on any path; the only |
| +// key it may hold is its home's (replicaKeyGate). |
| +func TestReplicaRowsGetNoPlatformKey(t *testing.T) { |
| + b := &ExecBackend{DataDir: t.TempDir()} |
| + rt := registry.Route{Host: "shop-au.example.com", Kind: "instance", Socket: "/run/carlos/shop-au.sock", Replica: true} |
| + envPath := filepath.Join(b.DataDir, rt.Host+".env") |
| + if err := os.WriteFile(envPath, []byte("CARLOS_ADMIN_TOKEN=t\n"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, err := b.ensureInstanceKeyFor(rt, b.pathsFor(rt)); err != nil || got != InstanceKeyReplica { |
| + t.Fatalf("launch ensure = %v, %v; want replica", got, err) |
| + } |
| + if got, err := b.BackfillInstanceKey(rt); err != nil || got != InstanceKeyReplica { |
| + t.Fatalf("backfill = %v, %v; want replica", got, err) |
| + } |
| + if raw, _ := os.ReadFile(envPath); len(keyLines(raw)) != 0 { |
| + t.Fatal("a replica row was given a platform key") |
| + } |
| +} |