carlos / platform Public

cli: refuse a relative -db on agent-exec routes (#70)

Paul Campbell c6e9ae435d8ea113e335d64f0105120de009b82a parent 3b2d53a
2 files changed, +86 −0
  • cli.go +12 −0
  • e2e_test.go +74 −0
diff --git a/cli.go b/cli.go
index 3d9ca38a7..aeca97f16 100644
--- a/cli.go
+++ b/cli.go
@@ -145,6 +145,18 @@ func runAdd(args []string) {
if *backing == registry.BackingUnit && *hibernate && *dbPath == "" {
log.Fatalf("add %s: -backing unit -hibernate requires -db <path> — the box-wide litestream unit can't find a unit tenant's database on its own (see ops.FleetDBs)", host)
}
+ // An agent-exec child gets no STATE_DIRECTORY — the activation
+ // contract's "relative -db resolves inside $STATE_DIRECTORY" convention
+ // is systemd's half, real only under -backing unit. On the exec path the
+ // activator consumes the stored path verbatim (Restore, Seed, Start's
+ // --db flag, the replicator), so a relative path resolves against a cwd
+ // nobody chose and every wake dies as an edge 503 discoverable only in
+ // the journal (issue #70). Refuse it here, at the only door that can
+ // introduce one — every other mint path builds absolute paths.
+ if *backing != registry.BackingUnit && *dbPath != "" && !filepath.IsAbs(*dbPath) {
+ log.Fatalf("add %s: -db %q is relative, and agent-exec children get no STATE_DIRECTORY to resolve it in — pass an absolute path (the default here is %s), or use -backing unit, whose StateDirectory= makes relative paths real",
+ host, *dbPath, provision.DefaultDBPath(dataDir(), host))
+ }
account := resolveAccount(*reg, *acct, *acctID)
// A `daemon` route may not hibernate: the capability means the platform
// owns the process (enable, start, restart-on-adopt), and hibernation is
diff --git a/e2e_test.go b/e2e_test.go
index 5071e7b82..97539e2c4 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -1353,6 +1353,80 @@ func TestAddUnitBackingRequiresDBForHibernate(t *testing.T) {
}
}
+// A relative -db on an agent-exec route must be refused at add time. The
+// activation contract's "relative -db resolves inside $STATE_DIRECTORY"
+// convention is real for unit tenants (systemd sets it) but agent-exec
+// children are spawned without it and the activator consumes the path
+// verbatim (Restore, Seed, Start --db, Replicate) — so a relative path
+// resolves against an unwritable cwd and every wake dies as an edge 503,
+// discoverable only by correlating 503s with child stderr in the journal
+// (issue #70, vitogo's first deploy).
+func TestAddRefusesRelativeDBOnExecRoutes(t *testing.T) {
+ goBin, err := exec.LookPath("go")
+ if err != nil {
+ t.Skip("go toolchain not found")
+ }
+ tmp := t.TempDir()
+ carlosBin := filepath.Join(tmp, "carlos")
+ build := exec.Command(goBin, "build", "-o", carlosBin, ".")
+ build.Env = os.Environ()
+ if out, err := build.CombinedOutput(); err != nil {
+ t.Fatalf("build carlos: %v\n%s", err, out)
+ }
+ regPath := filepath.Join(tmp, "registry.db")
+
+ run := func(args ...string) (string, error) {
+ cmd := exec.Command(carlosBin, args...)
+ cmd.Env = carlosTestEnv(t)
+ cmd.Dir = tmp
+ out, err := cmd.CombinedOutput()
+ return string(out), err
+ }
+
+ // The vitogo add, verbatim shape: exec backing (the default), relative -db.
+ out, err := run("add", "-registry", regPath, "-app", "vitogo",
+ "-kind", "instance", "-db", "vito.db", "-hibernate",
+ "-channel", "canary/vitogo", "vito.example.com")
+ if err == nil {
+ t.Fatalf("add with relative -db on an exec route: want non-zero exit, got 0\n%s", out)
+ }
+ // The refusal must carry the diagnosis and the way out — not just "no".
+ if !strings.Contains(out, "STATE_DIRECTORY") || !strings.Contains(out, "absolute") {
+ t.Errorf("refusal = %q, want it to explain STATE_DIRECTORY and require an absolute path", out)
+ }
+ // And it must refuse BEFORE writing: no half-registered route.
+ reg, err := registry.Open(regPath)
+ if err != nil {
+ t.Fatalf("open registry: %v", err)
+ }
+ defer reg.Close()
+ rt, err := reg.Get("vito.example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rt != nil {
+ t.Fatalf("refused add still wrote a route: %+v", rt)
+ }
+
+ // A unit tenant keeps the contract: StateDirectory= is real there, a
+ // relative -db is the documented convention, and this guard must not
+ // take it away.
+ out, err = run("add", "-registry", regPath, "-app", "vitogo",
+ "-kind", "instance", "-backing", "unit", "-db", "vito.db",
+ "unit.example.com")
+ if err != nil {
+ t.Fatalf("add -backing unit with relative -db: %v\n%s", err, out)
+ }
+
+ // An absolute -db on an exec route is untouched by the guard.
+ out, err = run("add", "-registry", regPath, "-app", "vitogo",
+ "-kind", "instance", "-db", "/data/vito.example.com.db",
+ "vito.example.com")
+ if err != nil {
+ t.Fatalf("add with absolute -db: %v\n%s", err, out)
+ }
+}
+
// TestAddAccountIDRefusesAName pins the whole -account-id gate end to end,
// on the verb where getting it wrong is quietest. `carlos add -account-id
// ops` used to succeed: "ops" passes release.ValidAccount (^[a-z]{3,16}$),