carlos / platform Public

appconfig: the exec sync pass applies each route's instance overlay (plan 4, task 6)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 1b31ae04b17db2e043ddbd475f2896249328a9be parent 05b7210
2 files changed, +128 −1
  • internal/appconfig/instances_test.go +99 −0
  • internal/appconfig/syncexec.go +29 −1
diff --git a/internal/appconfig/instances_test.go b/internal/appconfig/instances_test.go
index 0bf1b7d3d..76265ecf1 100644
--- a/internal/appconfig/instances_test.go
+++ b/internal/appconfig/instances_test.go
@@ -657,3 +657,102 @@ func TestSyncStillLeavesUnconfiguredAppsAlone(t *testing.T) {
t.Fatalf("an unconfigured app's file moved: %q %v", cur, err)
}
}
+
+// The exec half of the same delivery: one activator-spawned instance gets
+// its own value, its sibling does not, and the listing is paid for once.
+func TestSyncExecRendersTheInstanceOverlayForItsOwnRouteOnly(t *testing.T) {
+ ctx, base, reg, dd, privB64, _ := execKit(t)
+ priv, err := ParsePrivateKey(privB64, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
+ map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
+ t.Fatalf("ApplyInstance: %v", err)
+ }
+ for _, host := range []string{"book.example.com", "cart.example.com"} {
+ if err := reg.Upsert(registry.Route{
+ Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
+ Socket: "/run/carlos/" + host + ".sock",
+ }); err != nil {
+ t.Fatalf("Upsert %s: %v", host, err)
+ }
+ }
+
+ s := newCountingStore(base)
+ if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ book, err := os.ReadFile(filepath.Join(dd, "book.example.com.config.env"))
+ if err != nil {
+ t.Fatalf("read book config: %v", err)
+ }
+ if !bytes.Contains(book, []byte("TENANT=book\n")) || !bytes.Contains(book, []byte("APP_WIDE=yes\n")) {
+ t.Fatalf("the overlaid route's config file is wrong:\n%s", book)
+ }
+ cart, err := os.ReadFile(filepath.Join(dd, "cart.example.com.config.env"))
+ if err != nil {
+ t.Fatalf("read cart config: %v", err)
+ }
+ if bytes.Contains(cart, []byte("book")) || !bytes.Contains(cart, []byte("TENANT=none\n")) {
+ t.Fatalf("a sibling saw another instance's config:\n%s", cart)
+ }
+ if got := s.n(s.lists, InstancesConfigPrefix("bab", "titogo")); got != 1 {
+ t.Fatalf("instance overlay listings = %d, want exactly 1 for two routes of one app", got)
+ }
+ if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", "cart.example.com")); got != 0 {
+ t.Fatalf("read an overlay for a host the listing did not name (%d times)", got)
+ }
+}
+
+// exec_delivery is a BUNDLE-level flag and the instance layer is not one of
+// the bundles it is read from (the ruling at appconfig.go:66-71). A TCP-addr
+// route whose app has never opted in gets no file, whatever its overlay
+// says — a per-host delivery flag is exactly the granularity that ruling
+// refused, and honouring one here would be the config-that-lies hazard it
+// names.
+func TestSyncExecNeverReadsExecDeliveryFromTheInstanceLayer(t *testing.T) {
+ ctx, s, reg, dd, privB64, _ := execKit(t)
+ priv, err := ParsePrivateKey(privB64, "test")
+ if err != nil {
+ t.Fatalf("ParsePrivateKey: %v", err)
+ }
+ if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
+ t.Fatalf("SetEnv: %v", err)
+ }
+ putInstanceRaw(t, ctx, s, "bab", "titogo", "router.example.com", &Manifest{
+ Schema: 1, App: "titogo", Vars: map[string]Var{"TENANT": {Kind: "env", Value: "book"}},
+ ExecDelivery: ExecDeliveryFile})
+ if err := reg.Upsert(registry.Route{
+ Host: "router.example.com", Kind: "instance", App: "titogo", Account: "bab",
+ Status: "ready", Addr: "127.0.0.1:8080",
+ }); err != nil {
+ t.Fatalf("Upsert: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
+ t.Fatalf("SyncExec: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dd, "router.example.com.config.env")); !os.IsNotExist(err) {
+ t.Fatalf("an instance overlay opted a route into file delivery: %v", err)
+ }
+
+ // Opt the APP in, and the same route gets the same overlay merged in the
+ // ordinary way — the flag's door is the bundle, the overlay's payload is
+ // vars.
+ if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
+ t.Fatalf("SetExecDelivery: %v", err)
+ }
+ if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
+ t.Fatalf("SyncExec (opted in): %v", err)
+ }
+ cur, err := os.ReadFile(filepath.Join(dd, "router.example.com.config.env"))
+ if err != nil {
+ t.Fatalf("read router config: %v", err)
+ }
+ if !bytes.Contains(cur, []byte("TENANT=book\n")) {
+ t.Fatalf("the overlay's vars did not land once the app opted in:\n%s", cur)
+ }
+}
diff --git a/internal/appconfig/syncexec.go b/internal/appconfig/syncexec.go
index 5e520b447..f9180957d 100644
--- a/internal/appconfig/syncexec.go
+++ b/internal/appconfig/syncexec.go
@@ -379,6 +379,15 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
fmt.Printf(" config sync: all %d resolvable (non-errored) config groups on this box resolved absent — refusing to remove any config files this pass; check the registry's account column and the deployment bucket\n", eligible)
}
+ // The instance layer (senders design §6.2), resolved per route after the
+ // group base. Same loader, same cost, same failure contract as the unit
+ // pass's: one listing per app, a Get only for a host it named, and an
+ // unresolved overlay leaves the file exactly as it is.
+ overlayFor := instanceOverlayLoader(ctx, s)
+ // One line per app when its listing fails, not one per route: every
+ // route of that app fails identically and for the same reason.
+ said := map[groupKey]bool{}
+
for _, rt := range routes {
in, addrShape, sidecarShape := inScope(rt)
if !in {
@@ -405,6 +414,18 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
fmt.Printf(" %s: config unresolved (environment %q), config file left as-is: %v\n", rt.Host, env, r.err)
continue
}
+ // Fetched here, used at the bottom, because a failure must be able to
+ // stop a REMOVAL too: deciding a file is orphaned while unable to see
+ // whether this instance has an overlay would delete a live tenant's
+ // delivered secrets on a listing blip.
+ inst, _, instErr := overlayFor(k.acct, k.app, rt.Host)
+ if instErr != nil {
+ if !said[k] {
+ said[k] = true
+ fmt.Printf(" %s/%s: instance config unresolved, config files left as-is: %v\n", k.acct, k.app, instErr)
+ }
+ continue
+ }
if r.m == nil && skipRemovals {
continue
}
@@ -466,7 +487,14 @@ func SyncExec(ctx context.Context, reg *registry.Registry, s release.Store, priv
case sidecarShape:
hostBounce = nil
}
- syncExecHost(rt, r.m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, ack)
+ // The group's merge, overlaid by this instance's own layer. A nil
+ // base stays nil: an absent manifest is a REMOVAL on this path, and
+ // task 7 is the one case where an overlay changes that.
+ m := r.m
+ if m != nil {
+ m = applyInstanceOverlay(k.app, m, inst)
+ }
+ syncExecHost(rt, m, Key(k.acct, k.app), priv, dataDir, hostBounce, header, ack)
}
// Stale-secrets notice (issue #18 design ruling, ratified 2026-08-03
// review): an addr-shaped route whose app is DEFINITIVELY not opted in