carlos / platform Public

appconfig: SyncExec's third eligibility case — opted-in TCP-addr routes

Closes the platform half of issue #18: a TCP-addr exec route (Backing ==
"", Socket == "", Addr != "") whose app opted in (ExecDelivery == "file"
on its default bundle) now gets /data/<host>.config.env materialized
exactly as a socket-exec route does — same grouping, LoadMerged
environments-awareness, mass-removal guard, path-traversal guard, and
reap discipline, all via the same resolved-group machinery, no
addr-specific carve-out in any of it.

execSyncable splits into execRouteBase (the shared shape) plus
execSyncable (socket, unconditionally in scope) and execAddrCandidate
(addr, shape-only — opt-in gates actual inclusion). checkOptedIn reads
the app's default bundle once per pass (piggybacking on loadDefault's
existing cache), treating a fetch failure, a never-configured app, and an
unrecognized exec_delivery value alike as "not opted in" this pass —
loudly logging the last of those once per app, never aborting the pass
(additive discipline: a manifest a newer carlos wrote must not brick an
older box's sync). Opt-in itself requires the default bundle to exist, so
an opted-in group can never resolve to the mass-removal guard's "absent"
state by construction — it only ever converges to content or to empty.

Never bounces these routes, structurally: inScope's addrShape return
tells the second loop to pass nil, not the real bounce closure, into
syncExecHost — there is nothing left for that call to invoke, not a flag
it checks and skips. TestSyncExecOptedInAddrRouteIsNeverBounced pins this
across both a fresh write and a content change, either of which would
bounce a socket-exec route.

inScope is the single door both the group-resolution loop and the
materialization loop use, so they cannot diverge on which routes are in
scope or which are addr-shaped — closing the same divergence hazard the
existing "if they ever diverge" comment on the resolved-map lookup
already flags for execSyncable.
Paul Campbell 5728767a8ab4029e1e2baec9dc2fa163a4ed0add parent d0876a0
2 files changed, +543 −14
  • internal/appconfig/syncexec.go +125 −14
  • internal/appconfig/syncexec_test.go +418 −0
diff --git a/internal/appconfig/syncexec.go b/internal/appconfig/syncexec.go
index b6040135c..a8d18c438 100644
--- a/internal/appconfig/syncexec.go
+++ b/internal/appconfig/syncexec.go
@@ -8,6 +8,18 @@
// hand-staged-file migration hazard by construction. See spec
// 2026-08-02-exec-config-delivery-design.md.
//
+// A third eligibility case (issue #18): a TCP-addr exec route (Backing ==
+// "", Socket == "", Addr != "") whose app has opted in via its default
+// bundle's additive "exec_delivery": "file" manifest field
+// (Manifest.ExecDelivery) gets the identical file, materialized with the
+// same layering guarantees, environments-awareness, reap discipline, and
+// mass-removal/path-traversal guards as a socket-exec route — but is
+// NEVER bounced (see syncExecHost and execAddrCandidate): carlos routes
+// to these processes without spawning or owning them, so the restart half
+// belongs to the app's own supervisor. An un-opted TCP-addr route is
+// skipped exactly as before this case existed — see execAddrCandidate and
+// SyncExec's checkOptedIn closure.
+//
// Resolution mirrors sync.go's exactly — the group is (Account, App,
// Environment) and a route's bound environment is an overlay on the app's
// default bundle (environments.go, pipelines design §4a). Only the file
@@ -89,8 +101,60 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
defaults[ak] = d
return d
}
+ // optedIn caches the app-level exec_delivery decision so a box with
+ // many TCP-addr routes on one app doesn't cost more than one manifest
+ // read for it (loadDefault's own cache already dedupes the fetch; this
+ // dedupes the value-interpretation, including the unknown-value log,
+ // which must fire at most once per (acct, app) per pass, not once per
+ // route).
+ optedIn := map[appKey]bool{}
+ checkOptedIn := func(acct, app string) bool {
+ ak := appKey{acct, app}
+ if v, done := optedIn[ak]; done {
+ return v
+ }
+ def := loadDefault(acct, app)
+ v := false
+ switch {
+ case def.err != nil, !def.found:
+ // A fetch failure or a genuinely never-configured app both read
+ // as "not opted in" — the difference between them (retry next
+ // pass vs. steady state) doesn't matter here: neither is
+ // consulted below except as a false return, so nothing this
+ // pass does can distinguish them, and nothing needs to.
+ v = false
+ case def.m.ExecDelivery == ExecDeliveryFile:
+ v = true
+ case def.m.ExecDelivery == "":
+ v = false
+ default:
+ // A manifest a NEWER carlos wrote with a value THIS binary
+ // doesn't understand must not brick this box's sync (additive
+ // discipline, CLAUDE.md) — ignore it, loudly, once, and treat
+ // the app as not opted in rather than refusing the whole pass.
+ fmt.Printf(" %s/%s: config: exec_delivery %q in the default bundle is not a value this binary understands — ignoring it (not opted in); upgrade carlos if this is expected\n", acct, app, def.m.ExecDelivery)
+ v = false
+ }
+ optedIn[ak] = v
+ return v
+ }
+ // inScope is the single door both loops below use to decide whether a
+ // route is in scope this pass, and — for the addr shape — that it must
+ // never be bounced. One function so the two loops structurally cannot
+ // diverge on either question (see the "if they ever diverge" comment
+ // on the second loop's map lookup, which depends on the same key
+ // having been computed the same way here).
+ inScope := func(rt registry.Route) (ok, addrShape bool) {
+ if execSyncable(rt) {
+ return true, false
+ }
+ if execAddrCandidate(rt) && checkOptedIn(release.RouteAccount(rt), rt.App) {
+ return true, true
+ }
+ return false, false
+ }
for _, rt := range routes {
- if !execSyncable(rt) {
+ if in, _ := inScope(rt); !in {
continue
}
k := groupKey{release.RouteAccount(rt), rt.App, rt.Environment}
@@ -174,15 +238,16 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
}
for _, rt := range routes {
- if !execSyncable(rt) {
+ in, addrShape := inScope(rt)
+ if !in {
continue
}
k := groupKey{release.RouteAccount(rt), rt.App, rt.Environment}
// Insist the group was really resolved rather than taking the map's
// zero value: a zero resolution reads as "absent", which on this
- // path REMOVES the file. The two loops filter on the same
- // execSyncable today, so this cannot fire — but if they ever
- // diverge, this turns a silent config deletion into a skip.
+ // path REMOVES the file. The two loops filter through the same
+ // inScope today, so this cannot fire — but if they ever diverge,
+ // this turns a silent config deletion into a skip.
r, ok := resolved[k]
if !ok {
continue
@@ -208,7 +273,18 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
// resolves through LoadMerged, which errors on a missing bundle
// rather than returning nothing). A bound group's key would be
// dead in every branch that reads this.
- syncExecHost(rt, r.m, Key(k.acct, k.app), priv, dataDir, bounce)
+ //
+ // addrShape routes get nil in bounce's place, not the real
+ // closure with a runtime check skipping the call: carlos does not
+ // own an app-supervised process, so there is structurally nothing
+ // to hand syncExecHost that COULD invoke it for this route (issue
+ // #18 design comment) — bounceLogged's existing nil-is-a-no-op
+ // contract does the rest.
+ hostBounce := bounce
+ if addrShape {
+ hostBounce = nil
+ }
+ syncExecHost(rt, r.m, Key(k.acct, k.app), priv, dataDir, hostBounce)
}
// An empty route list is never authority to delete anything —
// registry.Open silently creates an empty database on a wrong path or
@@ -220,16 +296,44 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
return nil
}
-// execSyncable: lifecycle-managed instances the ACTIVATOR spawns — and
-// therefore whose environment this sweep can actually deliver. Mirrors
-// the unit path's syncable() exclusions, then narrows to exec routes
-// with a socket upstream: addr-upstream routes are proxy-only (the
-// process behind them is externally managed) and unit routes belong to
-// the root adopt path.
-func execSyncable(rt registry.Route) bool {
+// execRouteBase is the lifecycle-managed-instance shape both exec route
+// kinds share: the unit path's syncable() exclusions, narrowed to
+// non-unit backing (unit routes belong to the root adopt path). Neither
+// SyncExec eligibility case on its own — see execSyncable and
+// execAddrCandidate, which each narrow it by upstream shape.
+func execRouteBase(rt registry.Route) bool {
return rt.Kind == "instance" && rt.App != "" && !rt.Platform &&
(rt.Status == "" || rt.Status == "ready") &&
- rt.Backing != registry.BackingUnit && rt.Socket != ""
+ rt.Backing != registry.BackingUnit
+}
+
+// execSyncable: lifecycle-managed instances the ACTIVATOR spawns — and
+// therefore whose environment this sweep can actually deliver and whose
+// process it can bounce. Narrows execRouteBase to a socket upstream;
+// unconditionally in scope, opt-in or not (this is the original, PR #16
+// shape).
+func execSyncable(rt registry.Route) bool {
+ return execRouteBase(rt) && rt.Socket != ""
+}
+
+// execAddrCandidate: the TCP-addr exec shape (issue #18) — an
+// app-supervised instance family carlos routes to but neither spawns nor
+// owns the process of (titogo's router fleet is the motivating case:
+// titogo-router + controld own the instance processes, carlos just
+// proxies to 127.0.0.1:<port>). Narrows execRouteBase to an addr upstream
+// with no socket.
+//
+// This predicate is the ROUTE-SHAPE half only — it says nothing about
+// whether the route is actually in scope this pass. Unlike execSyncable,
+// that also requires the app's default bundle to carry "exec_delivery":
+// "file" (Manifest.ExecDelivery): the reader of the file this shape
+// produces is the app's OWN supervisor, not carlos, so writing it for an
+// app that never reads it would be dead weight at best and, if the app's
+// supervisor treats the file's mere presence or mtime as a restart
+// signal, an unwanted bounce at worst. See SyncExec's checkOptedIn/
+// inScope closures, which gate this predicate on that opt-in.
+func execAddrCandidate(rt registry.Route) bool {
+ return execRouteBase(rt) && rt.Socket == "" && rt.Addr != ""
}
// syncExecHost converges one route's config file. m == nil means the
@@ -241,6 +345,13 @@ func execSyncable(rt registry.Route) bool {
// key came back absent) and effect (which file got removed) land
// together on one line, instead of requiring a reader to cross-reference
// the fetch-side log.
+//
+// bounce may be nil: bounceLogged treats that as "nothing to call", which
+// is how a TCP-addr (issue #18) route's file gets converged or removed
+// exactly like a socket-exec route's — same write/rename/remove path,
+// same logging — while never restarting anything, because carlos does
+// not own that process. The caller (SyncExec) passes nil for those routes
+// structurally, not a flag this function branches on.
func syncExecHost(rt registry.Route, m *Manifest, key string, priv *ecdh.PrivateKey, dataDir string, bounce func(host string) error) {
if rt.Host == "" || rt.Host == "." || rt.Host == ".." || rt.Host != filepath.Base(rt.Host) {
fmt.Printf(" %q: not a bare hostname, refusing to derive a config file path from it\n", rt.Host)
diff --git a/internal/appconfig/syncexec_test.go b/internal/appconfig/syncexec_test.go
index 1c863c076..118cb3f8e 100644
--- a/internal/appconfig/syncexec_test.go
+++ b/internal/appconfig/syncexec_test.go
@@ -479,6 +479,61 @@ func TestSyncExecReapsOnlyItsOwnNamespace(t *testing.T) {
}
}
+// TestSyncExecReapsOnlyItsOwnNamespaceWithAddrShapeInTheMix is the reap
+// test's issue #18 twin: an opted-in addr route's own file survives reap
+// (it has a route row, same as any in-scope route), and a stray
+// *.config.env with NO route row at all is reaped regardless of which
+// exec shape produced it in some earlier pass — reap keys purely on
+// (namespace suffix, route existence), never on shape or opt-in.
+func TestSyncExecReapsOnlyItsOwnNamespaceWithAddrShapeInTheMix(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, _ := ParsePrivateKey(priv, "test")
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"X": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "live.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ files := map[string]bool{ // name -> should survive
+ "live.go.tito.io.config.env": true, // opted-in addr route, has a route row
+ "gone.go.tito.io.config.env": false, // no route row at all: reaped, whatever shape wrote it
+ }
+ for name := range files {
+ if err := os.WriteFile(filepath.Join(dd, name), []byte("X=1\n"), 0o600); err != nil {
+ t.Fatalf("stage %s: %v", name, err)
+ }
+ }
+ var bounced []string
+ if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error {
+ bounced = append(bounced, h)
+ return nil
+ }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ for name, keep := range files {
+ _, err := os.Stat(filepath.Join(dd, name))
+ if keep && err != nil {
+ t.Fatalf("%s: want kept, got %v", name, err)
+ }
+ if !keep && !os.IsNotExist(err) {
+ t.Fatalf("%s: want reaped, stat err = %v", name, err)
+ }
+ }
+ // The reap pass itself must never bounce — reap's log line says "no
+ // route on this box", never "bounce"; and the addr route's own
+ // convergence (a fresh, unchanged-content write above) must not
+ // bounce either.
+ if len(bounced) != 0 {
+ t.Fatalf("bounced = %v, want none", bounced)
+ }
+}
+
func TestSyncExecEmptyRegistryRemovesNothing(t *testing.T) {
ctx, s, reg, dd, priv, _ := execKit(t)
pk, _ := ParsePrivateKey(priv, "test")
@@ -510,6 +565,369 @@ func TestSyncExecRefusesNonBareHostname(t *testing.T) {
}
}
+// TestSyncExecOptedInAddrRouteMaterializesFile is issue #18's core case:
+// a TCP-addr route (Socket == "", Addr != "") whose app opted in via
+// ExecDeliveryFile in its default bundle gets the identical
+// <host>.config.env a socket-exec route would — same rendering, same
+// layering guarantees — even though carlos neither spawned nor owns the
+// process behind it.
+func TestSyncExecOptedInAddrRouteMaterializesFile(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"TITOGO_DEMO_BANNER": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env"))
+ if err != nil {
+ t.Fatalf("opted-in TCP-addr route got no config file: %v", err)
+ }
+ if string(got) != "TITOGO_DEMO_BANNER=1\n" {
+ t.Fatalf("config env = %q, want TITOGO_DEMO_BANNER=1", got)
+ }
+}
+
+// TestSyncExecUnoptedAddrRouteIsSkipped pins the un-opted case: the exact
+// same TCP-addr route shape, but its app never set exec_delivery, is
+// skipped exactly as before issue #18's third case existed — no file, no
+// error, nothing to converge or remove.
+func TestSyncExecUnoptedAddrRouteIsSkipped(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"TITOGO_DEMO_BANNER": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ // Deliberately no SetExecDelivery call.
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dd, "demo.go.tito.io.config.env")); !os.IsNotExist(err) {
+ t.Fatalf("un-opted TCP-addr route got a config file, stat err = %v", err)
+ }
+}
+
+// TestSyncExecUnrecognizedExecDeliveryValueIgnoredNotFatal covers additive
+// discipline (CLAUDE.md): a manifest a NEWER carlos wrote, carrying an
+// exec_delivery value this binary doesn't understand, must not brick this
+// box's sync — the whole pass must still succeed, the unknown value must
+// be treated as "not opted in" (not a fatal error, not silently treated
+// as "file"), and OTHER apps' routes in the same pass must converge
+// normally.
+func TestSyncExecUnrecognizedExecDeliveryValueIgnoredNotFatal(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ // Written directly, bypassing SetExecDelivery's set-time refusal — the
+ // shape of a manifest a newer binary produced.
+ raw := []byte(`{"schema":1,"app":"titogo","vars":{"K":{"kind":"env","value":"1"}},"exec_delivery":"grpc"}`)
+ if _, err := s.Put(ctx, Key("bab", "titogo"), raw, ""); err != nil {
+ t.Fatalf("Put: %v", err)
+ }
+ // A second, ordinary socket-exec app in the same pass, to prove the
+ // unknown value doesn't abort or otherwise disturb the rest of the
+ // sweep.
+ if err := SetEnv(ctx, s, "bab", "eleven", map[string]string{"LOG_LEVEL": "debug"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ routes := []registry.Route{
+ {Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", Status: "ready", Addr: "127.0.0.1:8444"},
+ {Host: "inst.example.com", Kind: "instance", App: "eleven", Account: "bab", Status: "ready", Socket: "/run/carlos/inst.sock"},
+ }
+ for _, rt := range routes {
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatalf("Upsert %s: %v", rt.Host, err)
+ }
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dd, "demo.go.tito.io.config.env")); !os.IsNotExist(err) {
+ t.Fatalf("unrecognized exec_delivery value was treated as opted-in, stat err = %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dd, "inst.example.com.config.env")); err != nil {
+ t.Fatalf("eleven's ordinary socket route did not converge despite titogo's unrecognized value: %v", err)
+ }
+}
+
+// TestSyncExecOptedInAddrRouteIsNeverBounced is the structural pin the
+// issue's design comment demands: carlos does not own an app-supervised
+// process, so an opted-in TCP-addr route's bounce callback must not even
+// be invoked — not "invoked and then skipped by a flag", genuinely never
+// called. The recorder below would append on any call at all; it must
+// stay empty across a pass that DOES change the route's file content
+// (proving the file write path ran) and a second pass where content is
+// unchanged (proving the still-quiet path also never bounces).
+func TestSyncExecOptedInAddrRouteIsNeverBounced(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"TITOGO_DEMO_BANNER": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ var bounced []string
+ bounce := func(h string) error {
+ bounced = append(bounced, h)
+ return nil
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil {
+ t.Fatalf("SyncExec (initial write): %v", err)
+ }
+ if len(bounced) != 0 {
+ t.Fatalf("bounced = %v, want none — carlos does not own this process", bounced)
+ }
+ // Change the config so the file's content changes on the next pass —
+ // the socket-exec equivalent of this WOULD bounce.
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"TITOGO_DEMO_BANNER": "2"}); err != nil {
+ t.Fatalf("SetEnv (change): %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil {
+ t.Fatalf("SyncExec (changed content): %v", err)
+ }
+ if len(bounced) != 0 {
+ t.Fatalf("bounced = %v after a content change, want none", bounced)
+ }
+ got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env"))
+ if err != nil {
+ t.Fatalf("read config env: %v", err)
+ }
+ if string(got) != "TITOGO_DEMO_BANNER=2\n" {
+ t.Fatalf("config env = %q, want the changed content (TITOGO_DEMO_BANNER=2) — the write path must still run", got)
+ }
+}
+
+// TestSyncExecSocketRouteStillBouncesAlongsideOptedInAddrRoute proves the
+// addr shape's no-bounce rule is scoped to addr routes ONLY: a socket-exec
+// route for a DIFFERENT app in the same pass must still bounce on change,
+// unaffected by the addr route sharing the sweep.
+func TestSyncExecSocketRouteStillBouncesAlongsideOptedInAddrRoute(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"A": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "eleven", map[string]string{"B": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ routes := []registry.Route{
+ {Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", Status: "ready", Addr: "127.0.0.1:8444"},
+ {Host: "inst.example.com", Kind: "instance", App: "eleven", Account: "bab", Status: "ready", Socket: "/run/carlos/inst.sock"},
+ }
+ for _, rt := range routes {
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatalf("Upsert %s: %v", rt.Host, err)
+ }
+ }
+ var bounced []string
+ bounce := func(h string) error {
+ bounced = append(bounced, h)
+ return nil
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, bounce); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ if !slices.Contains(bounced, "inst.example.com") {
+ t.Fatalf("bounced = %v, want the socket-exec route bounced (unaffected by the addr route sharing the pass)", bounced)
+ }
+ if slices.Contains(bounced, "demo.go.tito.io") {
+ t.Fatalf("bounced = %v, the opted-in addr route must never be bounced", bounced)
+ }
+ if len(bounced) != 1 {
+ t.Fatalf("bounced = %v, want exactly one bounce", bounced)
+ }
+}
+
+// TestSyncExecOptedInAddrRouteHonorsBoundEnvironment: environments-
+// awareness applies identically to the addr shape — a route bound to a
+// named environment resolves via LoadMerged (default overlaid by the
+// environment, environment winning per key), exactly like a socket-exec
+// route bound the same way.
+func TestSyncExecOptedInAddrRouteHonorsBoundEnvironment(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{
+ "LOG_LEVEL": "info",
+ "BASE_ONLY": "1",
+ }); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := SaveEnvironment(ctx, s, "bab", "titogo", "demo", map[string]Var{
+ "LOG_LEVEL": {Kind: "env", Value: "debug"},
+ "DEMO_ONLY": {Kind: "env", Value: "yes"},
+ }, nil); err != nil {
+ t.Fatalf("SaveEnvironment: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444", Environment: "demo",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env"))
+ if err != nil {
+ t.Fatalf("read config env: %v", err)
+ }
+ want := "BASE_ONLY=1\nDEMO_ONLY=yes\nLOG_LEVEL=debug\n"
+ if string(got) != want {
+ t.Fatalf("config env = %q, want %q", got, want)
+ }
+}
+
+// TestSyncExecOptedInAddrAppWithNoVarsConvergesToEmptyFile: opting in
+// (SetExecDelivery alone, no SetEnv) creates the default bundle with an
+// empty Vars map — found=true, so this is the "configured, then nothing
+// set" state, not "absent". The route must get an EMPTY file, not have
+// none written at all and not have the mass-removal guard's absent
+// bucket confused by it. This is also the reason
+// "opted-in AND the group resolves absent" is unreachable by
+// construction: checkOptedIn requires the default bundle to exist
+// (found=true) before an addr route is ever added to resolved, and a
+// present-but-empty bundle is exactly resolution{m: <empty manifest>},
+// never resolution{} (nil, absent).
+func TestSyncExecOptedInAddrAppWithNoVarsConvergesToEmptyFile(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := reg.Upsert(registry.Route{
+ Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8444",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env"))
+ if err != nil {
+ t.Fatalf("opted-in-but-unconfigured app's route got no file at all (want an empty one, not none): %v", err)
+ }
+ if string(got) != "" {
+ t.Fatalf("config env = %q, want empty", got)
+ }
+}
+
+// TestSyncExecMassRemovalGuardUnaffectedByOptedInAddrAppWithContent
+// exercises the mass-removal guard sharing its EXACT code path with the
+// addr shape (no addr-specific carve-out): an opted-in addr app WITH
+// content sits in the pass alongside a socket app that is genuinely
+// never configured. Only the socket app's group is absent — the guard
+// must not fire (only one of two eligible groups is absent, same as
+// TestSyncExecMassRemovalGuardDoesNotBlockNormalConvergence pins for two
+// socket apps), so the socket app's stale file is removed normally while
+// the addr app's file converges with its content, unaffected by sharing
+// the pass.
+func TestSyncExecMassRemovalGuardUnaffectedByOptedInAddrAppWithContent(t *testing.T) {
+ ctx, s, reg, dd, priv, _ := execKit(t)
+ pk, err := ParsePrivateKey(priv, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"A": "1"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ // eleven is never configured at all: genuinely absent.
+ routes := []registry.Route{
+ {Host: "demo.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", Status: "ready", Addr: "127.0.0.1:8444"},
+ {Host: "b.example.com", Kind: "instance", App: "eleven", Account: "bab", Status: "ready", Socket: "/run/carlos/b.sock"},
+ }
+ for _, rt := range routes {
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatalf("Upsert %s: %v", rt.Host, err)
+ }
+ }
+ pathEleven := filepath.Join(dd, "b.example.com.config.env")
+ if err := os.WriteFile(pathEleven, []byte("OLD=2\n"), 0o600); err != nil {
+ t.Fatalf("stage eleven: %v", err)
+ }
+ var bounced []string
+ if err := SyncExec(ctx, reg, s, pk, dd, func(h string) error {
+ bounced = append(bounced, h)
+ return nil
+ }); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ got, err := os.ReadFile(filepath.Join(dd, "demo.go.tito.io.config.env"))
+ if err != nil {
+ t.Fatalf("titogo's config file wasn't written: %v", err)
+ }
+ if string(got) != "A=1\n" {
+ t.Fatalf("titogo config env = %q, want A=1", got)
+ }
+ if _, err := os.Stat(pathEleven); !os.IsNotExist(err) {
+ t.Fatalf("eleven's config file survived its manifest's genuine absence, stat err = %v (guard incorrectly disarmed convergence, or incorrectly held it back)", err)
+ }
+ // eleven's file is a removal (bounceLogged fires); titogo's is a
+ // fresh write, socket-shape rules would bounce it too, but titogo is
+ // the addr shape — it must never appear here.
+ if !slices.Contains(bounced, "b.example.com") {
+ t.Fatalf("bounced = %v, want eleven's removal bounce", bounced)
+ }
+ if slices.Contains(bounced, "demo.go.tito.io") {
+ t.Fatalf("bounced = %v, the opted-in addr route must never be bounced", bounced)
+ }
+ if len(bounced) != 1 {
+ t.Fatalf("bounced = %v, want exactly one bounce", bounced)
+ }
+}
+
// TestSyncExecConvergesToEmptyFileWhenAllVarsUnset covers the lifecycle
// F8 flagged as missing: a manifest object that still EXISTS but has had
// every var unset must converge its route to an EMPTY file, not have the