Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions registry/coder/modules/mux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```
Expand All @@ -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
}
```
Expand All @@ -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,
Expand All @@ -65,27 +65,32 @@ 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"
}
```

### 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.
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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"
}
Expand All @@ -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"
}
Expand All @@ -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
}
Expand All @@ -164,7 +169,7 @@ Run without installing from the network (requires a `mux` binary at `<install_pr
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
install = false
}
Expand Down
51 changes: 51 additions & 0 deletions registry/coder/modules/mux/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,57 @@ chmod +x ${MODULE_ROOT}/mux`,
}
}, 60000);

it("passes additional_args before additional_arguments", async () => {
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",
Expand Down
21 changes: 20 additions & 1 deletion registry/coder/modules/mux/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
}

Expand Down Expand Up @@ -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,
Expand Down
41 changes: 39 additions & 2 deletions registry/coder/modules/mux/mux.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"
}
}

Expand Down
16 changes: 14 additions & 2 deletions registry/coder/modules/mux/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading