From faf7a063a6b7023d0cd43d1f78b4656c04719e57 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 22 Oct 2026 15:00:40 +0000 Subject: [PATCH 1/2] feat(coder/mux): add additional_args, deprecate additional_arguments Add an additional_args map of flag to value for `mux server`, matching the additional_args name used across the registry. Each flag is passed exactly as written (--long or -s, the admin's choice), followed by its value as the next argument; an empty value passes the flag alone. --port and --add-project are rejected, since the module sets them. additional_arguments keeps working unchanged and is appended after additional_args; it is marked deprecated, for use only where a map cannot express the arguments (a repeated flag). Bumps to 2.1.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2TMgCPW8SL8kUCB28L4dt --- registry/coder/modules/mux/README.md | 45 ++++++++++++------- registry/coder/modules/mux/main.test.ts | 54 +++++++++++++++++++++++ registry/coder/modules/mux/main.tf | 25 ++++++++++- registry/coder/modules/mux/mux.tftest.hcl | 37 +++++++++++++++- registry/coder/modules/mux/run.sh | 16 ++++++- 5 files changed, 155 insertions(+), 22 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 977cbbaad..c24c54940 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -14,7 +14,7 @@ Automatically install and run [Mux](https://github.com/coder/xum) in a Coder wor module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id } ``` @@ -37,7 +37,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id } ``` @@ -48,7 +48,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id # Default is "next"; set to a specific version to pin. # Only versions published as @coder/xum are available: 0.28.3 or newer, @@ -65,7 +65,7 @@ Start Mux with `mux server --add-project /path/to/project`: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id add_project = "/path/to/project" } @@ -73,19 +73,30 @@ module "mux" { ### Pass Arbitrary `mux server` Arguments -Use `additional_arguments` to append additional arguments to `mux server`. -The module parses quoted values, so grouped arguments remain intact. +Use `additional_args` to append flags to `mux server`, as a map of flag to +value. Each flag is passed exactly as written, so use `--name` or `-n` as +`mux server` expects, followed by its value as the next argument; an empty +value passes the flag alone. Values are passed verbatim, so spaces need no +quoting. Terraform orders map keys alphabetically. `--port` and +`--add-project` are rejected; use `port` and `add_project` instead. ```tf module "mux" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" - agent_id = coder_agent.main.id - additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'" + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "2.1.0" + agent_id = coder_agent.main.id + additional_args = { + "--open-mode" = "pinned" + } } ``` +`additional_arguments` (a shell-style string) is deprecated in favor of +`additional_args` but still works, and is appended after `additional_args`. +Use it only for what a map cannot express, such as a flag repeated more than +once. + ### Restart After Mux Exits Enable automatic restarts after Mux exits, including clean exits and intentional shutdown signals such as `SIGTERM`. The launcher waits for `restart_delay_seconds`, removes `~/.mux/server.lock`, and starts Mux again. Set `max_restart_attempts` to a whole number to stop retrying after a fixed number of restarts, or leave it at `0` for unlimited retries. @@ -94,7 +105,7 @@ Enable automatic restarts after Mux exits, including clean exits and intentional module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id restart_on_kill = true restart_delay_seconds = 3 @@ -108,7 +119,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id port = 8080 } @@ -122,7 +133,7 @@ Force a specific package manager instead of auto-detection: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id package_manager = "pnpm" # or "npm", "bun" } @@ -136,7 +147,7 @@ Use a private or mirrored npm registry. The registry must serve the scoped `@cod module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id registry_url = "https://npm.pkg.github.com" } @@ -150,7 +161,7 @@ Run an existing copy of Mux if found, otherwise install from npm: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "2.0.0" + version = "2.1.0" agent_id = coder_agent.main.id use_cached = true } @@ -164,7 +175,7 @@ Run without installing from the network (requires a `mux` binary at ` { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install: false, + additional_args: JSON.stringify({ + "--open-mode": "pinned tab", + "-v": "", + }), + additional_arguments: "--add-project '/workspaces/my repo'", + }); + + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer("alpine/curl"); + + try { + const setup = await execContainer(id, [ + "sh", + "-c", + `apk add --no-cache bash >/dev/null +mkdir -p ${MODULE_ROOT} +cat <<'EOF' > ${MODULE_ROOT}/mux +#!/usr/bin/env sh +i=1 +for arg in "$@"; do + echo "arg$i=$arg" + i=$((i + 1)) +done +EOF +chmod +x ${MODULE_ROOT}/mux`, + ]); + expect(setup.exitCode).toBe(0); + + const output = await execContainer(id, ["sh", "-c", instance.script]); + if (output.exitCode !== 0) { + console.log("STDOUT:\n" + output.stdout); + console.log("STDERR:\n" + output.stderr); + } + expect(output.exitCode).toBe(0); + + await execContainer(id, ["sh", "-c", "sleep 1"]); + const log = await readFileContainer(id, LOG_PATH); + expect(log).toContain("arg1=server"); + expect(log).toContain("arg2=--port"); + expect(log).toContain("arg3=4000"); + expect(log).toContain("arg4=--open-mode"); + expect(log).toContain("arg5=pinned tab"); + expect(log).toContain("arg6=-v"); + expect(log).toContain("arg7=--add-project"); + expect(log).toContain("arg8=/workspaces/my repo"); + } finally { + await removeContainer(id); + } + }, 60000); + it("logs signal-based exits after startup", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index 787df6934..b000952af 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -83,9 +83,27 @@ variable "add_project" { default = null } +variable "additional_args" { + description = "Additional flags to pass to `mux server`, as a map of flag to value. Each flag is passed as written (e.g. `--open-mode` or `-o`) followed by its value as the next argument; an empty value passes the flag alone (e.g. `{ \"--open-mode\" = \"pinned\" }`)." + type = map(string) + default = {} + + validation { + condition = alltrue([for k, v in var.additional_args : can(regex("^--?[A-Za-z0-9][A-Za-z0-9._-]*$", k)) && !can(regex("\n", v))]) + error_message = "additional_args keys must be flags such as --name or -n (letters, digits, '.', '_', '-'), and values must not contain newlines." + } + + # The module already sets these from port and add_project. Keep this list + # in sync with the `mux server` flags in run.sh. + validation { + condition = length(setintersection(keys(var.additional_args), ["--port", "--add-project"])) == 0 + error_message = "additional_args must not set --port or --add-project; use port and add_project instead." + } +} + variable "additional_arguments" { type = string - description = "Additional command-line arguments to pass to `mux server` (for example: `--add-project /path --open-mode pinned`)." + description = "Deprecated: use `additional_args` instead. A shell-style string, still honored and appended after `additional_args`; use it only for arguments a flag map cannot express, such as a repeated flag." default = "" } @@ -181,6 +199,10 @@ resource "random_password" "mux_auth_token" { } locals { + # Render additional_args as argv: each key as written, followed by its + # value as a separate argument unless the value is empty. + additional_args = flatten([for k, v in var.additional_args : v == "" ? [k] : [k, v]]) + mux_auth_token = random_password.mux_auth_token.result registry_url = trimsuffix(var.registry_url, "/") } @@ -194,6 +216,7 @@ resource "coder_script" "mux" { PORT : var.port, LOG_PATH : var.log_path, ADD_PROJECT : var.add_project == null ? "" : var.add_project, + ADDITIONAL_ARGS : base64encode(join("\n", local.additional_args)), ADDITIONAL_ARGUMENTS : var.additional_arguments, INSTALL_PREFIX : var.install_prefix, OFFLINE : !var.install, diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index 4a0db7edc..07994a365 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -79,7 +79,40 @@ run "auth_token_in_url" { } } -run "custom_additional_arguments" { +run "custom_additional_args" { + command = plan + + variables { + agent_id = "foo" + additional_args = { + "--open-mode" = "pinned" + "-v" = "" + } + } + + assert { + condition = local.additional_args == ["--open-mode", "pinned", "-v"] + error_message = "each flag must be passed as written, followed by its value unless the value is empty" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, base64encode("--open-mode\npinned\n-v")) + error_message = "mux launch script must include the encoded additional_args" + } +} + +run "additional_args_rejects_module_flags" { + command = plan + + variables { + agent_id = "foo" + additional_args = { "--port" = "8080" } + } + + expect_failures = [var.additional_args] +} + +run "deprecated_additional_arguments_still_applied" { command = plan variables { @@ -89,7 +122,7 @@ run "custom_additional_arguments" { assert { condition = strcontains(resource.coder_script.mux.script, "--open-mode pinned --add-project '/workspaces/my repo'") - error_message = "mux launch script must include the configured additional arguments" + error_message = "additional_arguments must still be passed to mux server" } } diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index 839a07c21..057065d3c 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -31,13 +31,25 @@ function run_mux() { mkdir -p "$(dirname "${LOG_PATH}")" - # Build args for mux (POSIX-compatible, avoid bash arrays) + # Build args for mux (POSIX-compatible, avoid bash arrays). Keep these + # flags in sync with the reserved-flag list in additional_args' validation + # in main.tf, so additional_args cannot set them a second time. set -- server --port "$port_value" if [ -n "${ADD_PROJECT}" ]; then set -- "$@" --add-project "${ADD_PROJECT}" fi - # Parse additional user-supplied server arguments while preserving quoted groups. + # additional_args: one argument per line, base64-encoded. + if [ -n "${ADDITIONAL_ARGS}" ]; then + while IFS= read -r parsed_arg || [ -n "$parsed_arg" ]; do + [ -n "$parsed_arg" ] || continue + set -- "$@" "$parsed_arg" + done << EOF_ADDITIONAL_ARGS +$(printf '%s' "${ADDITIONAL_ARGS}" | base64 -d) +EOF_ADDITIONAL_ARGS + fi + + # Parse the deprecated additional_arguments string while preserving quoted groups. if [ -n "${ADDITIONAL_ARGUMENTS}" ]; then local parsed_additional_arguments if ! parsed_additional_arguments="$(printf "%s\n" "${ADDITIONAL_ARGUMENTS}" | xargs -n1 printf "%s\n" 2> /dev/null)"; then From b787c98afdb1534581780ae7d367ae38abc05412 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 23 Oct 2026 03:06:05 +0000 Subject: [PATCH 2/2] feat(coder/mux): make additional_args a list of strings Per review on coder/registry#1202, a map cannot repeat a flag (a repeated key silently keeps only the last value). additional_args is now a list with one element per argument, passed in order. --port and --add-project are still rejected, with or without =value. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01R2TMgCPW8SL8kUCB28L4dt --- registry/coder/modules/mux/README.md | 24 ++++++++------------- registry/coder/modules/mux/main.test.ts | 5 +---- registry/coder/modules/mux/main.tf | 20 +++++++---------- registry/coder/modules/mux/mux.tftest.hcl | 26 +++++++++++++---------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index c24c54940..a14e681f7 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -73,29 +73,23 @@ module "mux" { ### Pass Arbitrary `mux server` Arguments -Use `additional_args` to append flags to `mux server`, as a map of flag to -value. Each flag is passed exactly as written, so use `--name` or `-n` as -`mux server` expects, followed by its value as the next argument; an empty -value passes the flag alone. Values are passed verbatim, so spaces need no -quoting. Terraform orders map keys alphabetically. `--port` and +Use `additional_args` to append arguments to `mux server`, as a list with one +element per argument. Each element is passed exactly as written and in order, +so a flag can repeat and a value can contain spaces without quoting. `--port` and `--add-project` are rejected; use `port` and `add_project` instead. ```tf module "mux" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/mux/coder" - version = "2.1.0" - agent_id = coder_agent.main.id - additional_args = { - "--open-mode" = "pinned" - } + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "2.1.0" + agent_id = coder_agent.main.id + additional_args = ["--open-mode", "pinned"] } ``` `additional_arguments` (a shell-style string) is deprecated in favor of -`additional_args` but still works, and is appended after `additional_args`. -Use it only for what a map cannot express, such as a flag repeated more than -once. +`additional_args`. It still works, and is appended after `additional_args`. ### Restart After Mux Exits diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index 60507376e..4f06ae0fa 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -126,10 +126,7 @@ chmod +x ${MODULE_ROOT}/mux`, const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", install: false, - additional_args: JSON.stringify({ - "--open-mode": "pinned tab", - "-v": "", - }), + additional_args: JSON.stringify(["--open-mode", "pinned tab", "-v"]), additional_arguments: "--add-project '/workspaces/my repo'", }); diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index b000952af..fd3034d61 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -84,26 +84,26 @@ variable "add_project" { } variable "additional_args" { - description = "Additional flags to pass to `mux server`, as a map of flag to value. Each flag is passed as written (e.g. `--open-mode` or `-o`) followed by its value as the next argument; an empty value passes the flag alone (e.g. `{ \"--open-mode\" = \"pinned\" }`)." - type = map(string) - default = {} + description = "Additional arguments to pass to `mux server`, one list element per argument, in order (e.g. `[\"--open-mode\", \"pinned\"]`). Each element is passed as written, so a flag can repeat and a value may contain spaces." + type = list(string) + default = [] validation { - condition = alltrue([for k, v in var.additional_args : can(regex("^--?[A-Za-z0-9][A-Za-z0-9._-]*$", k)) && !can(regex("\n", v))]) - error_message = "additional_args keys must be flags such as --name or -n (letters, digits, '.', '_', '-'), and values must not contain newlines." + condition = alltrue([for a in var.additional_args : a != "" && !can(regex("\n", a))]) + error_message = "additional_args elements must be non-empty and must not contain newlines." } # The module already sets these from port and add_project. Keep this list # in sync with the `mux server` flags in run.sh. validation { - condition = length(setintersection(keys(var.additional_args), ["--port", "--add-project"])) == 0 + condition = !anytrue([for a in var.additional_args : contains(["--port", "--add-project"], split("=", a)[0])]) error_message = "additional_args must not set --port or --add-project; use port and add_project instead." } } variable "additional_arguments" { type = string - description = "Deprecated: use `additional_args` instead. A shell-style string, still honored and appended after `additional_args`; use it only for arguments a flag map cannot express, such as a repeated flag." + description = "Deprecated: use `additional_args` instead. A shell-style string, still honored and appended after `additional_args`." default = "" } @@ -199,10 +199,6 @@ resource "random_password" "mux_auth_token" { } locals { - # Render additional_args as argv: each key as written, followed by its - # value as a separate argument unless the value is empty. - additional_args = flatten([for k, v in var.additional_args : v == "" ? [k] : [k, v]]) - mux_auth_token = random_password.mux_auth_token.result registry_url = trimsuffix(var.registry_url, "/") } @@ -216,7 +212,7 @@ resource "coder_script" "mux" { PORT : var.port, LOG_PATH : var.log_path, ADD_PROJECT : var.add_project == null ? "" : var.add_project, - ADDITIONAL_ARGS : base64encode(join("\n", local.additional_args)), + ADDITIONAL_ARGS : base64encode(join("\n", var.additional_args)), ADDITIONAL_ARGUMENTS : var.additional_arguments, INSTALL_PREFIX : var.install_prefix, OFFLINE : !var.install, diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index 07994a365..757986487 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -83,22 +83,26 @@ run "custom_additional_args" { command = plan variables { - agent_id = "foo" - additional_args = { - "--open-mode" = "pinned" - "-v" = "" - } + agent_id = "foo" + additional_args = ["--open-mode", "pinned", "-v", "-v"] } + # One argument per line, in order, with the repeated flag kept. assert { - condition = local.additional_args == ["--open-mode", "pinned", "-v"] - error_message = "each flag must be passed as written, followed by its value unless the value is empty" + condition = strcontains(resource.coder_script.mux.script, base64encode("--open-mode\npinned\n-v\n-v")) + error_message = "mux launch script must include the encoded additional_args" } +} - assert { - condition = strcontains(resource.coder_script.mux.script, base64encode("--open-mode\npinned\n-v")) - error_message = "mux launch script must include the encoded additional_args" +run "additional_args_rejects_newline" { + command = plan + + variables { + agent_id = "foo" + additional_args = ["--open-mode\npinned"] } + + expect_failures = [var.additional_args] } run "additional_args_rejects_module_flags" { @@ -106,7 +110,7 @@ run "additional_args_rejects_module_flags" { variables { agent_id = "foo" - additional_args = { "--port" = "8080" } + additional_args = ["--port=8080"] } expect_failures = [var.additional_args]