package appconfig import ( "context" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func testRegistry(t *testing.T) *registry.Registry { t.Helper() reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatalf("open registry: %v", err) } return reg } func TestSyncWritesFileAndRestartsOnChange(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, pub, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } pubKey, err := ParsePublicKey(pub, "test") if err != nil { t.Fatalf("ParsePublicKey: %v", err) } envelope, err := Seal(pubKey, Binding{Account: "bab", App: "keymail"}, []byte("shh")) if err != nil { t.Fatalf("Seal: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := SetSecret(ctx, s, "bab", "keymail", map[string]string{"SES_SECRET": envelope}); err != nil { t.Fatalf("SetSecret: %v", err) } if err := reg.Upsert(registry.Route{ Host: "keymail.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, }); err != nil { t.Fatalf("Upsert: %v", err) } var restarted []string restart := func(host string) error { restarted = append(restarted, host); return nil } if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(filepath.Join(envDir, "keymail.example.com.env")) if err != nil { t.Fatalf("read rendered env: %v", err) } want := "LOG_LEVEL=debug\nSES_SECRET=shh\nCARLOS_PURGE=header\n" if string(got) != want { t.Fatalf("rendered env = %q, want %q", got, want) } if len(restarted) != 1 || restarted[0] != "keymail.example.com" { t.Fatalf("restarted = %v, want exactly [keymail.example.com]", restarted) } // A second pass with nothing changed must not write or restart again. restarted = nil if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync (second pass): %v", err) } if len(restarted) != 0 { t.Fatalf("second pass restarted %v, want none", restarted) } } func TestSyncSkipsRoutesWithNoConfigEverSet(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "bare.example.com", Kind: "instance", App: "bareapp", Account: "bab", Status: "ready", }); err != nil { t.Fatalf("Upsert: %v", err) } var restarted []string restart := func(host string) error { restarted = append(restarted, host); return nil } if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync: %v", err) } if _, err := os.Stat(filepath.Join(envDir, "bare.example.com.env")); !os.IsNotExist(err) { t.Fatalf("expected no env file for a route with no config ever set, stat err = %v", err) } if len(restarted) != 0 { t.Fatalf("restarted = %v, want none", restarted) } } func TestSyncLeavesHandStagedFileAloneWhenNoManifestExists(t *testing.T) { // The migration case: an app whose env file is still hand-staged on the // host, with nothing in the bucket for it yet. Turning this feature on // (setting CARLOS_SECRETS_KEY) must not truncate that file — "no // manifest object at all" is a no-op, not "converge to empty". s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "legacy.example.com", Kind: "instance", App: "legacyapp", Account: "bab", Status: "ready", }); err != nil { t.Fatalf("Upsert: %v", err) } path := filepath.Join(envDir, "legacy.example.com.env") staged := "DATABASE_URL=postgres://staged\nAPI_TOKEN=hand-written\n" if err := os.WriteFile(path, []byte(staged), 0o600); err != nil { t.Fatalf("stage env file: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read staged env: %v", err) } if string(got) != staged { t.Fatalf("hand-staged env file = %q, want it untouched (%q)", got, staged) } if len(restarted) != 0 { t.Fatalf("restarted = %v, want none", restarted) } } func TestSyncConvergesToAgentOwnedOnlyAfterEveryVarIsUnset(t *testing.T) { // The mirror of the case above: the manifest object DOES exist, it just // has no vars left. That is a real, operator-intended state and must // converge — every tenant var is dropped and the unit restarted. The // file is not fully emptied: CARLOS_PURGE=header is agent-owned, not a // tenant var, and applyAgentVars appends it unconditionally. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := reg.Upsert(registry.Route{ Host: "keymail.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, }); err != nil { t.Fatalf("Upsert: %v", err) } var restarted []string restart := func(h string) error { restarted = append(restarted, h); return nil } if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync: %v", err) } path := filepath.Join(envDir, "keymail.example.com.env") if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { t.Fatalf("after set: env = %q, err = %v; want LOG_LEVEL=debug\\nCARLOS_PURGE=header", got, err) } if _, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"}); err != nil { t.Fatalf("Unset: %v", err) } restarted = nil if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync (after unset): %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read env after unset: %v", err) } // Convergence to empty is now "just the agent-owned line" — the agent // still owns CARLOS_PURGE regardless of what the tenant's manifest holds. if string(got) != "CARLOS_PURGE=header\n" { t.Fatalf("after full unset: env = %q, want just CARLOS_PURGE=header", got) } if len(restarted) != 1 || restarted[0] != "keymail.example.com" { t.Fatalf("restarted = %v, want exactly [keymail.example.com]", restarted) } } func TestSyncReapsEnvFilesForHostsWithNoRoute(t *testing.T) { // A decommissioned route's env file may hold decrypted secrets; nothing // else on the box removes it. A host that still has a route row keeps // its file even if it has no config set at all. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "live.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", }); err != nil { t.Fatalf("Upsert: %v", err) } // A route that exists but is not syncable (mid-provisioning) must also // keep its file — reaping keys off the full route list, not syncable(). if err := reg.Upsert(registry.Route{ Host: "pending.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "provisioning", }); err != nil { t.Fatalf("Upsert: %v", err) } files := map[string]string{ "live.example.com.env": "KEEP=1\n", "pending.example.com.env": "KEEP=2\n", "gone.example.com.env": "SECRET=leftover\n", } for name, body := range files { if err := os.WriteFile(filepath.Join(envDir, name), []byte(body), 0o600); err != nil { t.Fatalf("stage %s: %v", name, err) } } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } if _, err := os.Stat(filepath.Join(envDir, "gone.example.com.env")); !os.IsNotExist(err) { t.Fatalf("orphaned env file survived Sync, stat err = %v", err) } for _, name := range []string{"live.example.com.env", "pending.example.com.env"} { got, err := os.ReadFile(filepath.Join(envDir, name)) if err != nil { t.Fatalf("%s: want it kept, read err = %v", name, err) } if string(got) != files[name] { t.Fatalf("%s = %q, want it untouched (%q)", name, got, files[name]) } } } func TestSyncEmptyRegistryLeavesEnvFilesAlone(t *testing.T) { // registry.Open silently creates an empty database when handed a wrong // path or an unmounted /data — "successful read, zero routes" is an // easily reachable state, not proof the box runs nothing. Reaping // against it would wipe every env file on the box as root. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) // empty: no routes ever upserted envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } staged := "DATABASE_URL=postgres://staged\n" path := filepath.Join(envDir, "handstaged.example.com.env") if err := os.WriteFile(path, []byte(staged), 0o600); err != nil { t.Fatalf("stage env file: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("env file gone after Sync against an empty registry: %v", err) } if string(got) != staged { t.Fatalf("env file = %q, want it untouched (%q)", got, staged) } } func TestSyncSkipsExecBackedRoutes(t *testing.T) { // An exec-backed route (Backing == "", the fleet default) reads // /.env, not /etc/carlos/apps — materializing there // would write a file nothing reads and claim a restart that restarted // nothing. Sync must skip it (with a log line, not silence). s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := reg.Upsert(registry.Route{ Host: "execshape.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", }); err != nil { t.Fatalf("Upsert: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } if _, err := os.Stat(filepath.Join(envDir, "execshape.example.com.env")); !os.IsNotExist(err) { t.Fatalf("expected no env file for an exec-backed route, stat err = %v", err) } if len(restarted) != 0 { t.Fatalf("restarted = %v, want none", restarted) } } func TestSyncReapsStaleTmpFiles(t *testing.T) { // A crash between write and rename leaves .env.tmp holding // decrypted secrets; by the time reap runs, any temp file is stale by // construction and must go — even one whose host still has a route. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "live.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, }); err != nil { t.Fatalf("Upsert: %v", err) } for _, name := range []string{"live.example.com.env.tmp", "gone.example.com.env.tmp"} { if err := os.WriteFile(filepath.Join(envDir, name), []byte("SECRET=leftover\n"), 0o600); err != nil { t.Fatalf("stage %s: %v", name, err) } } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } for _, name := range []string{"live.example.com.env.tmp", "gone.example.com.env.tmp"} { if _, err := os.Stat(filepath.Join(envDir, name)); !os.IsNotExist(err) { t.Fatalf("%s survived Sync, stat err = %v", name, err) } } } func TestSyncHostRefusesNonBareHostname(t *testing.T) { // A registry row whose Host carries a path separator must never place a // root-written file outside envDir. envDir := filepath.Join(t.TempDir(), "apps") if err := os.Mkdir(envDir, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } m := &Manifest{Schema: 1, App: "evil", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}} for _, host := range []string{"../escape", "a/b", "", ".", ".."} { syncHost(registry.Route{Host: host}, m, false, nil, envDir, func(string) error { return nil }, "", nil) } if _, err := os.Stat(filepath.Join(envDir, "..", "escape.env")); !os.IsNotExist(err) { t.Fatalf("a path-escaping host wrote outside envDir, stat err = %v", err) } ents, err := os.ReadDir(envDir) if err != nil { t.Fatalf("read envDir: %v", err) } if len(ents) != 0 { t.Fatalf("envDir = %v, want nothing written for non-bare hostnames", ents) } } func TestSyncSurvivesAMissingEnvDir(t *testing.T) { // A box that has never materialized anything has no env dir; the reap // step must not turn that into an error. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := filepath.Join(t.TempDir(), "never-created") priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "bare.example.com", Kind: "instance", App: "bareapp", Account: "bab", Status: "ready", }); err != nil { t.Fatalf("Upsert: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync with a missing env dir: %v", err) } } func TestSyncRendersEachRoutesBoundEnvironment(t *testing.T) { // One app, three routes of it, two of them bound to different named // environments. Each route's file must be the default bundle overlaid by // ITS OWN environment — no leakage of one route's environment into // another's render (the per-(acct, app) fetch cache must not collapse // routes that resolve differently). s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "info", "SHARED": "yes"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, "DEMO_ONLY": {Kind: "env", Value: "1"}, }, nil); err != nil { t.Fatalf("SaveEnvironment demo: %v", err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "staging", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "trace"}, "STAGING_ONLY": {Kind: "env", Value: "1"}, }, nil); err != nil { t.Fatalf("SaveEnvironment staging: %v", err) } routes := map[string]string{ // host -> bound environment "plain.example.com": "", "demo.example.com": "demo", "staging.example.com": "staging", } for host, env := range routes { if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: env, }); err != nil { t.Fatalf("Upsert %s: %v", host, err) } } var restarted []string restart := func(h string) error { restarted = append(restarted, h); return nil } if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync: %v", err) } want := map[string]string{ "plain.example.com": "LOG_LEVEL=info\nSHARED=yes\nCARLOS_PURGE=header\n", "demo.example.com": "DEMO_ONLY=1\nLOG_LEVEL=debug\nSHARED=yes\nCARLOS_PURGE=header\n", "staging.example.com": "LOG_LEVEL=trace\nSHARED=yes\nSTAGING_ONLY=1\nCARLOS_PURGE=header\n", } for host, body := range want { got, err := os.ReadFile(filepath.Join(envDir, host+".env")) if err != nil { t.Fatalf("%s: read rendered env: %v", host, err) } if string(got) != body { t.Fatalf("%s: rendered env = %q, want %q", host, got, body) } } if len(restarted) != 3 { t.Fatalf("restarted = %v, want all three routes", restarted) } // Unchanged merged output on a second pass: no rewrite, no restart — // including for the environment-bound routes, whose merge must be stable. before := map[string]os.FileInfo{} for host := range want { fi, err := os.Stat(filepath.Join(envDir, host+".env")) if err != nil { t.Fatalf("%s: stat: %v", host, err) } before[host] = fi } restarted = nil if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync (second pass): %v", err) } if len(restarted) != 0 { t.Fatalf("second pass restarted %v, want none", restarted) } for host, fi := range before { now, err := os.Stat(filepath.Join(envDir, host+".env")) if err != nil { t.Fatalf("%s: stat after second pass: %v", host, err) } if !now.ModTime().Equal(fi.ModTime()) { t.Fatalf("%s: env file was rewritten on an unchanged pass", host) } } } func TestSyncLeavesEnvFileUntouchedWhenBoundEnvironmentIsMissing(t *testing.T) { // The bundle was deleted out from under a bound route. That route's // resolution FAILS — and a failed resolution must never reach the // render-and-diff path, because an empty render differs from the file on // disk and would truncate it as root and restart the unit. The last-good // file stays byte-identical and untouched; other routes of the same app // still converge. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "info"}); err != nil { t.Fatalf("SetEnv: %v", err) } for host, env := range map[string]string{ "gone.example.com": "vanished", "ok.example.com": "", } { if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: env, }); err != nil { t.Fatalf("Upsert %s: %v", host, err) } } lastGood := "LOG_LEVEL=debug\nDB_URL=postgres://last-good\n" path := filepath.Join(envDir, "gone.example.com.env") if err := os.WriteFile(path, []byte(lastGood), 0o600); err != nil { t.Fatalf("stage last-good env file: %v", err) } fi, err := os.Stat(path) if err != nil { t.Fatalf("stat staged file: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("last-good env file gone after Sync: %v", err) } if string(got) != lastGood { t.Fatalf("env file = %q, want it untouched (%q)", got, lastGood) } now, err := os.Stat(path) if err != nil { t.Fatalf("stat after Sync: %v", err) } if !now.ModTime().Equal(fi.ModTime()) { t.Fatalf("env file of a route with a missing environment was rewritten") } if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) { t.Fatalf("a temp file was written for a route whose environment is missing, stat err = %v", err) } for _, h := range restarted { if h == "gone.example.com" { t.Fatalf("restarted a route whose bound environment is missing: %v", restarted) } } // The sibling route must be entirely unaffected by the other's failure. sibling, err := os.ReadFile(filepath.Join(envDir, "ok.example.com.env")) if err != nil { t.Fatalf("sibling route not materialized: %v", err) } if string(sibling) != "LOG_LEVEL=info\nCARLOS_PURGE=header\n" { t.Fatalf("sibling env = %q, want LOG_LEVEL=info + CARLOS_PURGE=header", sibling) } if len(restarted) != 1 || restarted[0] != "ok.example.com" { t.Fatalf("restarted = %v, want exactly [ok.example.com]", restarted) } } func TestSyncDoesNotReapEnvFileWhenEnvironmentIsDeleted(t *testing.T) { // Reaping is keyed on ROUTES, never on environments: deleting a bundle // must not make a live route's env file look orphaned. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := reg.Upsert(registry.Route{ Host: "bound.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "deleted-env", }); err != nil { t.Fatalf("Upsert: %v", err) } path := filepath.Join(envDir, "bound.example.com.env") if err := os.WriteFile(path, []byte("KEEP=1\n"), 0o600); err != nil { t.Fatalf("stage env file: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("env file reaped after its environment was deleted: %v", err) } if string(got) != "KEEP=1\n" { t.Fatalf("env file = %q, want it untouched", got) } } // Review CRITICAL 1: binding an environment must not be a way to replace a // hand-staged production env file. The unbound path is protected by // getIfExists ("no config.json at all ⇒ leave the file alone"); a bound route // resolves through LoadMerged, which treats a missing default as an empty // base — so without this guard, the documented migration step (create a small // override bundle, bind the routes) replaces every var the staged file held // with just the override's. func TestSyncRefusesToReplaceStagedFileWhenAppHasNoDefaultBundle(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } // An override bundle only — the app has never had a config.json. if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } for host, env := range map[string]string{ "prod.example.com": "demo", "fresh.example.com": "demo", } { if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: env, }); err != nil { t.Fatalf("Upsert %s: %v", host, err) } } staged := "DB_URL=postgres://prod\nSTRIPE_KEY=sk_live\nSMTP_HOST=mail\nLOG_LEVEL=info\n" path := filepath.Join(envDir, "prod.example.com.env") if err := os.WriteFile(path, []byte(staged), 0o600); err != nil { t.Fatalf("stage env file: %v", err) } fi, err := os.Stat(path) if err != nil { t.Fatalf("stat staged file: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("staged env file gone after Sync: %v", err) } if string(got) != staged { t.Fatalf("staged env file = %q, want it untouched (%q)", got, staged) } if now, err := os.Stat(path); err != nil || !now.ModTime().Equal(fi.ModTime()) { t.Fatalf("staged env file was rewritten (stat err = %v)", err) } for _, h := range restarted { if h == "prod.example.com" { t.Fatalf("restarted a route whose staged file was left in place: %v", restarted) } } // The route with nothing staged has nothing to lose: environment-only // render proceeds for it, in the same pass. fresh, err := os.ReadFile(filepath.Join(envDir, "fresh.example.com.env")) if err != nil { t.Fatalf("fresh route not materialized: %v", err) } if string(fresh) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { t.Fatalf("fresh route env = %q, want LOG_LEVEL=debug + CARLOS_PURGE=header", fresh) } if len(restarted) != 1 || restarted[0] != "fresh.example.com" { t.Fatalf("restarted = %v, want exactly [fresh.example.com]", restarted) } } // Round-2 review, IMPORTANT: the no-default guard must not freeze a route on // the file SYNC ITSELF wrote. "A non-empty file exists" is not evidence a // human staged it — after pass 1 it is evidence of pass 1 — so the guard is // keyed on KEY LOSS instead: it refuses only when the file on disk holds keys // the merged render would drop. Same keys with different values, and supersets // of what is on disk, converge freely. func TestSyncEnvironmentOnlyAppKeepsConvergingAfterItsOwnFirstWrite(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } // No config.json for this app, ever — only a named bundle. if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } if err := reg.Upsert(registry.Route{ Host: "fresh.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "demo", }); err != nil { t.Fatalf("Upsert: %v", err) } var restarted []string restart := func(h string) error { restarted = append(restarted, h); return nil } if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync (pass 1): %v", err) } path := filepath.Join(envDir, "fresh.example.com.env") if got, err := os.ReadFile(path); err != nil || string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { t.Fatalf("pass 1: env = %q, err = %v; want LOG_LEVEL=debug + CARLOS_PURGE=header", got, err) } // The operator edits the bundle: one value changed, one key added. if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "trace"}, "NEW": {Kind: "env", Value: "1"}, }, nil); err != nil { t.Fatalf("SaveEnvironment (edit): %v", err) } restarted = nil if err := Sync(ctx, reg, s, privKey, envDir, restart, nil); err != nil { t.Fatalf("Sync (pass 2): %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("pass 2: read env: %v", err) } if string(got) != "LOG_LEVEL=trace\nNEW=1\nCARLOS_PURGE=header\n" { t.Fatalf("pass 2: env = %q, want the edited bundle to converge", got) } if len(restarted) != 1 || restarted[0] != "fresh.example.com" { t.Fatalf("pass 2: restarted = %v, want exactly [fresh.example.com]", restarted) } } // A file this pass cannot parse is content whose keys are unknown, so it is // impossible to prove nothing would be lost — assume the worst and refuse, // same direction as the guard's other cases. func TestSyncRefusesToReplaceAnUnparseableFileWhenAppHasNoDefaultBundle(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } if err := reg.Upsert(registry.Route{ Host: "odd.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "demo", }); err != nil { t.Fatalf("Upsert: %v", err) } junk := "this file is not KEY=value at all\n" path := filepath.Join(envDir, "odd.example.com.env") if err := os.WriteFile(path, []byte(junk), 0o600); err != nil { t.Fatalf("stage junk env file: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read env: %v", err) } if string(got) != junk { t.Fatalf("env = %q, want an unparseable file left untouched", got) } if len(restarted) != 0 { t.Fatalf("restarted = %v, want none", restarted) } } // Known limitation of the key-loss gate, pinned so it is not a surprise: // while an app has NO default bundle, REMOVING a key from its environment // bundle looks exactly like the truncation hazard (a key on disk that the // render drops) and is refused. The refusal names both escapes — put the // remaining config in a default bundle, or delete the file. It self-heals the // moment a default bundle exists, which is the migration this guard is for. func TestSyncEnvOnlyKeyRemovalIsRefusedUntilADefaultBundleExists(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "KEEP": {Kind: "env", Value: "1"}, "GONE": {Kind: "env", Value: "2"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } if err := reg.Upsert(registry.Route{ Host: "shrink.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "demo", }); err != nil { t.Fatalf("Upsert: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync (pass 1): %v", err) } path := filepath.Join(envDir, "shrink.example.com.env") if _, err := UnsetEnvironment(ctx, s, "bab", "keymail", "demo", []string{"GONE"}); err != nil { t.Fatalf("UnsetEnvironment: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync (pass 2): %v", err) } if got, _ := os.ReadFile(path); string(got) != "GONE=2\nKEEP=1\nCARLOS_PURGE=header\n" { t.Fatalf("env = %q, want the removal refused while the app has no default bundle", got) } // The moment a default bundle exists the app is bucket-managed and the // merged render is authoritative — the removal lands, no operator // intervention beyond the import the refusal asked for. if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"KEEP": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync (pass 3): %v", err) } if got, _ := os.ReadFile(path); string(got) != "KEEP=1\nCARLOS_PURGE=header\n" { t.Fatalf("env = %q, want the removal to land once a default bundle exists", got) } } func TestSyncMergedRenderIsAuthoritativeOnceADefaultBundleExists(t *testing.T) { // The other side of the guard above: once the app HAS a config.json — // even one with every var unset — the operator has adopted bucket config // for it, and the merged render is authoritative over whatever is on // disk. Anything else would freeze a migrated app's file forever. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"SEED": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } if _, err := Unset(ctx, s, "bab", "keymail", []string{"SEED"}); err != nil { t.Fatalf("Unset: %v", err) // object now exists with zero vars } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "LOG_LEVEL": {Kind: "env", Value: "debug"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } if err := reg.Upsert(registry.Route{ Host: "prod.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "demo", }); err != nil { t.Fatalf("Upsert: %v", err) } path := filepath.Join(envDir, "prod.example.com.env") if err := os.WriteFile(path, []byte("DB_URL=postgres://prod\n"), 0o600); err != nil { t.Fatalf("stage env file: %v", err) } var restarted []string if err := Sync(ctx, reg, s, privKey, envDir, func(h string) error { restarted = append(restarted, h) return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(path) if err != nil { t.Fatalf("read env: %v", err) } if string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { t.Fatalf("env = %q, want the merged render to win once a default bundle exists", got) } if len(restarted) != 1 || restarted[0] != "prod.example.com" { t.Fatalf("restarted = %v, want exactly [prod.example.com]", restarted) } } func TestSyncEnvironmentOnlyAppWithNoDefaultBundle(t *testing.T) { // A named environment does not require a default bundle to exist: // LoadMerged resolves to the environment alone, and Sync must converge it // (the object exists, so this is a configured app, not an unconfigured one). s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "ONLY": {Kind: "env", Value: "here"}, }, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } for _, host := range []string{"demo.example.com", "emptyfile.example.com"} { if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, Environment: "demo", }); err != nil { t.Fatalf("Upsert %s: %v", host, err) } } // An EMPTY file on disk is not staged config — there is nothing in it to // lose, so the no-default guard (CRITICAL 1) must not freeze it. if err := os.WriteFile(filepath.Join(envDir, "emptyfile.example.com.env"), nil, 0o600); err != nil { t.Fatalf("stage empty env file: %v", err) } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } for _, host := range []string{"demo.example.com", "emptyfile.example.com"} { got, err := os.ReadFile(filepath.Join(envDir, host+".env")) if err != nil { t.Fatalf("%s: read rendered env: %v", host, err) } if string(got) != "ONLY=here\nCARLOS_PURGE=header\n" { t.Fatalf("%s: rendered env = %q, want ONLY=here + CARLOS_PURGE=header", host, got) } } } func TestLostKeysIgnoresAgentOwnedVars(t *testing.T) { // The sync itself appends CARLOS_PURGE=header to every file it writes // (applyAgentVars). If lostKeys counts it, every defaultMissing route // the sync previously converged reads as "about to lose a key" and is // refused forever — the false-refusal loop. cur := []byte("A=1\nCARLOS_PURGE=header\n") m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}} lost, parsed := lostKeys(cur, m) if !parsed || len(lost) != 0 { t.Fatalf("lost=%v parsed=%v, want none/true", lost, parsed) } } // TestSyncHostConvergesAgentOwnedVarWithoutRewrite pins the convergence // property applyAgentVars must not break: syncHost's bytes.Equal // short-circuit (sync.go) has to still hold once every render carries an // appended CARLOS_PURGE=header line. The first pass writes the file // (containing the var) and restarts; an identical second pass must leave the // file's mtime and content alone and must NOT call restart again — proof // that applyAgentVars is deterministic and idempotent, not just that its // output happens to look right once. func TestSyncHostConvergesAgentOwnedVarWithoutRewrite(t *testing.T) { envDir := t.TempDir() m := &Manifest{Schema: 1, App: "keymail", Vars: map[string]Var{"A": {Kind: "env", Value: "1"}}} rt := registry.Route{Host: "steady.example.com", Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit} var restarted int restart := func(string) error { restarted++; return nil } syncHost(rt, m, false, nil, envDir, restart, "", nil) path := filepath.Join(envDir, "steady.example.com.env") got, err := os.ReadFile(path) if err != nil { t.Fatalf("read env after first sync: %v", err) } if string(got) != "A=1\nCARLOS_PURGE=header\n" { t.Fatalf("env = %q, want A=1 + CARLOS_PURGE=header", got) } if restarted != 1 { t.Fatalf("restarted = %d after first sync, want 1", restarted) } fi, err := os.Stat(path) if err != nil { t.Fatalf("stat after first sync: %v", err) } // Second, identical pass. syncHost(rt, m, false, nil, envDir, restart, "", nil) if restarted != 1 { t.Fatalf("restarted = %d after second identical sync, want still 1 (no re-restart)", restarted) } now, err := os.Stat(path) if err != nil { t.Fatalf("stat after second sync: %v", err) } if !now.ModTime().Equal(fi.ModTime()) { t.Fatalf("env file was rewritten on an unchanged pass (mtime moved)") } got2, err := os.ReadFile(path) if err != nil { t.Fatalf("read env after second sync: %v", err) } if string(got2) != string(got) { t.Fatalf("second pass content = %q, want unchanged (%q)", got2, got) } } func TestSyncMaterializesEveryRouteOfAMultiRouteApp(t *testing.T) { // One app, two simultaneous routes (e.g. a stable route and a canary // route are two different Hosts) — the shared config must land in // both routes' env files, per spec §5's per-route-host materialization. s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := testRegistry(t) envDir := t.TempDir() priv, _, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } privKey, err := ParsePrivateKey(priv, "test") if err != nil { t.Fatalf("ParsePrivateKey: %v", err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } for _, host := range []string{"stable.keymail.example.com", "canary.keymail.example.com"} { if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "keymail", Account: "bab", Status: "ready", Backing: registry.BackingUnit, }); err != nil { t.Fatalf("Upsert %s: %v", host, err) } } if err := Sync(ctx, reg, s, privKey, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } for _, host := range []string{"stable.keymail.example.com", "canary.keymail.example.com"} { got, err := os.ReadFile(filepath.Join(envDir, host+".env")) if err != nil { t.Fatalf("%s: read rendered env: %v", host, err) } if string(got) != "LOG_LEVEL=debug\nCARLOS_PURGE=header\n" { t.Fatalf("%s: rendered env = %q, want LOG_LEVEL=debug + CARLOS_PURGE=header", host, got) } } }