carlos / platform Public

cli/e2e: carlos env exec-delivery — issue #18's opt-in verb

Its own verb surface, not carlos env set -app <a> EXEC_DELIVERY=...:
exec_delivery isn't a config var (never rendered into an EnvironmentFile,
never listed alongside KEY/VALUE pairs), it's a manifest-level flag with
its own validation and its own app-level-only restriction, so it gets its
own parsing instead of a special-cased key name inside env set's KV
parser.

`carlos env exec-delivery -app <a> file|off` accepts -environment purely
so a mistaken attempt gets a specific refusal (explaining exec_delivery
is app-level only, read from the default bundle alone) rather than a
generic "unknown flag" — there is no environment-scoped path to route it
to, since appconfig.SetExecDelivery has no environment parameter at all.
A plain `env list` (no -environment) now prints an "exec-delivery: file"
line when the field is set; a named environment's listing never mentions
it, matching where it can ever actually be written.

TestExecDeliveryCLI follows TestEnvCLI's build-a-real-binary pattern:
round trip (on, mentioned in list, off, mention gone, existing vars
untouched throughout), unknown-value refusal, named-environment-placement
refusal (and confirms no environment bundle got written), and the
never-configured-app "off" refusal mirroring `env unset`'s existing
idiom.
Paul Campbell b660dbb3441cd9d5c5f4c1d48b4a150a9b65b442 parent 5728767
2 files changed, +176 −4
  • appconfig_cli.go +71 −4
  • e2e_test.go +105 −0
diff --git a/appconfig_cli.go b/appconfig_cli.go
index a00157af2..0aaeec0f3 100644
--- a/appconfig_cli.go
+++ b/appconfig_cli.go
@@ -21,15 +21,25 @@ import (
const envUsage = `usage: carlos env <command>
commands:
- set -app <a> [-environment <name>] KEY=value [KEY2=value2 ...] set one or more plain config vars
- unset -app <a> [-environment <name>] KEY [KEY2 ...] remove config vars
- list -app <a> [-environment <name>] list an app's config vars
- environments -app <a> list an app's named environment bundles
+ set -app <a> [-environment <name>] KEY=value [KEY2=value2 ...] set one or more plain config vars
+ unset -app <a> [-environment <name>] KEY [KEY2 ...] remove config vars
+ list -app <a> [-environment <name>] list an app's config vars
+ environments -app <a> list an app's named environment bundles
+ exec-delivery -app <a> file|off opt an app's TCP-addr exec routes into config delivery (issue #18)
-environment names a bundle at apps/<acct>/<app>/environments/<name>.json,
layered on top of the default bundle (config.json) at materialization time —
see LoadMerged. Omit -environment for the default bundle itself. The bundle
is created implicitly on its first "set", same as config.json today.
+
+exec-delivery is APP-LEVEL ONLY (never -environment — see runEnvExecDelivery):
+it sets/clears "exec_delivery" in the default bundle alone. "file" makes
+SyncExec write /data/<host>.config.env for the app's TCP-addr exec routes
+(routes carlos proxies to but does not spawn — e.g. titogo's router fleet)
+exactly as it already does for socket-exec routes, but those routes are
+NEVER bounced: carlos does not own the process, so the app's own supervisor
+owns the restart. "off" clears the field; a plain "env list" mentions it
+when set.
`
// RunEnv is the `carlos env` verb.
@@ -47,6 +57,8 @@ func RunEnv(args []string) {
runEnvList(args[1:])
case "environments":
runEnvEnvironments(args[1:])
+ case "exec-delivery":
+ runEnvExecDelivery(args[1:])
default:
fmt.Fprint(os.Stderr, envUsage)
os.Exit(1)
@@ -213,6 +225,14 @@ func runEnvList(args []string) {
log.Fatal(err)
}
fmt.Printf("%s (%s):\n", *app, bundleLabel(*environment))
+ // exec_delivery is app-level only (read from the default bundle
+ // alone — see appconfig.Manifest.ExecDelivery) and never printed for
+ // a named environment's listing, matching where it can ever actually
+ // be SET (runEnvExecDelivery refuses -environment before it reaches
+ // appconfig at all).
+ if *environment == "" && m.ExecDelivery != "" {
+ fmt.Printf("exec-delivery: %s\n", m.ExecDelivery)
+ }
var keys []string
for k, v := range m.Vars {
if v.Kind == "env" {
@@ -228,6 +248,53 @@ func runEnvList(args []string) {
w.Flush()
}
+// runEnvExecDelivery is `carlos env exec-delivery` — issue #18's opt-in
+// verb, deliberately NOT `env set -app <a> EXEC_DELIVERY=...`: the field
+// is not a config var (it's never rendered into an EnvironmentFile, never
+// listed alongside KEY/VALUE pairs), it's a manifest-level flag with its
+// own validation and its own restriction to the default bundle, so it
+// gets its own verb surface instead of overloading env set's KEY=value
+// parsing with a special-cased key name.
+func runEnvExecDelivery(args []string) {
+ fs := flag.NewFlagSet("env exec-delivery", flag.ExitOnError)
+ app := fs.String("app", "", "app name")
+ // -environment is accepted (matching every other env subcommand's
+ // flag set) purely so a mistaken attempt gets a clear, specific
+ // refusal here rather than a generic "unknown flag" — see the check
+ // below. There is no environment-scoped variant to route it to:
+ // appconfig.SetExecDelivery has no environment parameter at all.
+ environment := fs.String("environment", "", "refused: exec_delivery is app-level only (default bundle), never a named environment")
+ acctOf := accountFlags(fs)
+ fs.Parse(args)
+ if *app == "" || fs.NArg() != 1 {
+ log.Fatal("usage: carlos env exec-delivery -app <a> file|off")
+ }
+ if *environment != "" {
+ log.Fatal(`carlos env exec-delivery: -environment is refused — exec_delivery is APP-LEVEL ONLY, read from the ` +
+ "default bundle (config.json) alone. A named environment's manifest is never consulted for it (SyncExec " +
+ "ignores it there with a log, per the issue #18 design comment's \"config-that-lies\" rationale: the reader " +
+ "is the app's own supervisor, one program per app, not per-environment) — so setting it on one would " +
+ "silently do nothing useful. Omit -environment.")
+ }
+ var value string
+ switch fs.Arg(0) {
+ case appconfig.ExecDeliveryFile:
+ value = appconfig.ExecDeliveryFile
+ case "off":
+ value = ""
+ default:
+ log.Fatalf("carlos env exec-delivery: %q is not a valid value — want %q or \"off\"", fs.Arg(0), appconfig.ExecDeliveryFile)
+ }
+ if err := appconfig.SetExecDelivery(context.Background(), openStore(), acctOf(), *app, value); err != nil {
+ log.Fatal(err)
+ }
+ if value == "" {
+ fmt.Printf("exec-delivery off for %s\n", *app)
+ } else {
+ fmt.Printf("exec-delivery %s for %s\n", value, *app)
+ }
+}
+
func runEnvEnvironments(args []string) {
fs := flag.NewFlagSet("env environments", flag.ExitOnError)
app := fs.String("app", "", "app name")
diff --git a/e2e_test.go b/e2e_test.go
index c49e0670f..ec7c817f9 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -1606,6 +1606,111 @@ func TestEnvironmentBundlesCLI(t *testing.T) {
}
}
+// TestExecDeliveryCLI exercises `carlos env exec-delivery` (issue #18):
+// the app-level opt-in that lets SyncExec materialize
+// /data/<host>.config.env for TCP-addr exec routes — round-trip, unknown
+// value refusal, and named-environment placement refusal, following
+// TestEnvCLI's build-a-real-binary pattern.
+func TestExecDeliveryCLI(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)
+ }
+ storeDir := filepath.Join(tmp, "store")
+ env := append(os.Environ(), "CARLOS_DEPLOYMENT_DIR="+storeDir)
+ run := func(args ...string) string {
+ t.Helper()
+ cmd := exec.Command(carlosBin, args...)
+ cmd.Env = env
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("carlos %s: %v\n%s", strings.Join(args, " "), err, out)
+ }
+ return string(out)
+ }
+ runFails := func(args ...string) string {
+ t.Helper()
+ cmd := exec.Command(carlosBin, args...)
+ cmd.Env = env
+ out, err := cmd.CombinedOutput()
+ if err == nil {
+ t.Fatalf("carlos %s: succeeded, want a refusal\n%s", strings.Join(args, " "), out)
+ }
+ return string(out)
+ }
+
+ // Round trip: off by default (not mentioned in a plain list), "file"
+ // turns it on and a plain `env list` mentions it, "off" clears it
+ // again and the mention disappears.
+ run("env", "set", "-account", "bab", "-app", "titogo", "K=1")
+ out := run("env", "list", "-account", "bab", "-app", "titogo")
+ if strings.Contains(out, "exec-delivery") {
+ t.Fatalf("env list before opt-in = %q, must not mention exec-delivery", out)
+ }
+
+ out = run("env", "exec-delivery", "-account", "bab", "-app", "titogo", "file")
+ if !strings.Contains(out, "titogo") || !strings.Contains(out, "file") {
+ t.Fatalf("env exec-delivery file = %q, want a confirmation naming the app and the value", out)
+ }
+ out = run("env", "list", "-account", "bab", "-app", "titogo")
+ if !regexp.MustCompile(`(?m)^exec-delivery:\s*file$`).MatchString(out) {
+ t.Fatalf("env list after opt-in = %q, want an exec-delivery: file line", out)
+ }
+ // The var set earlier survives — exec-delivery is a field write, not a
+ // bundle replace.
+ if !regexp.MustCompile(`(?m)^K\s+1$`).MatchString(out) {
+ t.Fatalf("env list after opt-in = %q, lost K=1 (exec-delivery must not clobber existing vars)", out)
+ }
+
+ out = run("env", "exec-delivery", "-account", "bab", "-app", "titogo", "off")
+ if !strings.Contains(out, "off") {
+ t.Fatalf("env exec-delivery off = %q, want a confirmation", out)
+ }
+ out = run("env", "list", "-account", "bab", "-app", "titogo")
+ if strings.Contains(out, "exec-delivery") {
+ t.Fatalf("env list after off = %q, must no longer mention exec-delivery", out)
+ }
+
+ // Unknown values are refused at set time, naming the bad value.
+ msg := runFails("env", "exec-delivery", "-account", "bab", "-app", "titogo", "grpc")
+ if !strings.Contains(msg, "grpc") {
+ t.Fatalf("env exec-delivery grpc = %q, want a refusal naming the bad value", msg)
+ }
+ msg = runFails("env", "exec-delivery", "-account", "bab", "-app", "titogo", "on") // not the spelling
+ if !strings.Contains(msg, "on") {
+ t.Fatalf("env exec-delivery on = %q, want a refusal naming the bad value", msg)
+ }
+
+ // Named-environment placement is refused before it ever reaches the
+ // store: -environment is app-level only for this verb.
+ msg = runFails("env", "exec-delivery", "-account", "bab", "-app", "titogo", "-environment", "demo", "file")
+ if !strings.Contains(strings.ToLower(msg), "app-level") {
+ t.Fatalf("env exec-delivery -environment demo file = %q, want a refusal explaining it's app-level only", msg)
+ }
+ store := release.NewDirStore(storeDir)
+ if _, _, err := store.Get(context.Background(), "apps/bab/titogo/environments/demo.json"); err == nil {
+ t.Fatal("env exec-delivery -environment demo wrote a named environment bundle despite the refusal")
+ }
+
+ // Turning off against an app that was never configured at all is
+ // refused, not a silent create — same idiom as `env unset` on a
+ // nonexistent bundle.
+ msg = runFails("env", "exec-delivery", "-account", "bab", "-app", "titogo-never-configured", "off")
+ if !strings.Contains(msg, "nothing to turn off") {
+ t.Fatalf("env exec-delivery off on a never-configured app = %q, want a refusal", msg)
+ }
+ if _, _, err := store.Get(context.Background(), "apps/bab/titogo-never-configured/config.json"); err == nil {
+ t.Fatal("env exec-delivery off on a never-configured app created its config.json")
+ }
+}
+
// TestEnvironmentMergeVisibleFromCLI is the merge-visible check the plan
// asks for: set the default bundle plus a named override through the CLI
// exactly as an operator would, then verify — via the library's LoadMerged,