diff --git a/registry/coder/modules/agent-helper/README.md b/registry/coder/modules/agent-helper/README.md new file mode 100644 index 000000000..62eb3573d --- /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 000000000..6c1325896 --- /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 000000000..aa1314f70 --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tf @@ -0,0 +1,202 @@ +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 = "" +} + +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) + + log_file_creation_script_name = "${var.agent_name}-log_file_creation_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 = "$HOME/pre_install.log" + install_log_path = "$HOME/install.log" + post_install_log_path = "$HOME/post_install.log" + start_log_path = "$HOME/start.log" +} + +resource "coder_script" "log_file_creation_script" { + agent_id = var.agent_id + display_name = "Log File Creation Script" + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.log_file_creation_script_name}' EXIT + coder exp sync start ${local.log_file_creation_script_name} + + mkdir -p ${local.module_dir_path} + %{if var.pre_install_script != null~} + touch ${local.pre_install_log_path} + %{endif~} + touch ${local.install_log_path} + %{if var.post_install_script != null~} + touch ${local.post_install_log_path} + %{endif~} + touch ${local.start_log_path} + EOT +} + +resource "coder_script" "pre_install_script" { + count = var.pre_install_script != null ? 1 : 0 + depends_on = [coder_script.log_file_creation_script] + agent_id = var.agent_id + display_name = "Pre-Install Script" + run_on_start = true + log_path = local.pre_install_log_path + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT + coder exp sync want ${local.pre_install_script_name} ${local.log_file_creation_script_name} + 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} + EOT +} + +resource "coder_script" "install_script" { + agent_id = var.agent_id + depends_on = [coder_script.log_file_creation_script] + display_name = "Install Script" + log_path = local.install_log_path + run_on_start = true + script = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + + 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} + %{else~} + coder exp sync want ${local.install_script_name} ${local.log_file_creation_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} + EOT +} + +resource "coder_script" "post_install_script" { + count = var.post_install_script != null ? 1 : 0 + depends_on = [coder_script.log_file_creation_script] + agent_id = var.agent_id + display_name = "Post-Install Script" + log_path = local.post_install_log_path + 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} + EOT +} + +resource "coder_script" "start_script" { + agent_id = var.agent_id + depends_on = [coder_script.log_file_creation_script] + display_name = "Start Script" + log_path = local.start_log_path + 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} + EOT +} \ 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 000000000..5fc10716b --- /dev/null +++ b/registry/coder/modules/agent-helper/main.tftest.hcl @@ -0,0 +1,224 @@ +# 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 log file creation script is created + assert { + condition = coder_script.log_file_creation_script.agent_id == "test-agent-id" + error_message = "Log file creation script agent ID should match input" + } + + assert { + condition = coder_script.log_file_creation_script.display_name == "Log File Creation Script" + error_message = "Log file creation script should have correct display name" + } + + assert { + condition = coder_script.log_file_creation_script.run_on_start == true + error_message = "Log file creation script should run on 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" + } + + # 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" + } + + # 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" + } +} + +# 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.log_file_creation_script.agent_id == "test-agent-id" + error_message = "Log file creation script should be 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" + } +} + +# 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-log_file_creation_script", coder_script.log_file_creation_script.script)) + error_message = "Log file creation script should use custom agent_name in 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" + } +}