diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 977cbbaad..a14e681f7 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,24 @@ 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 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.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`. It still works, and is appended after `additional_args`. + ### 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 +99,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 +113,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 +127,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 +141,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 +155,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 +169,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..fd3034d61 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 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 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 = !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 = "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`." default = "" } @@ -194,6 +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", 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 4a0db7edc..757986487 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -79,7 +79,44 @@ 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", "-v"] + } + + # One argument per line, in order, with the repeated flag kept. + assert { + 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" + } +} + +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" { + 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 +126,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