refactor(agentapi): use coder-utils and tftpl for install script

Replace the inline coder_script resource with coder-utils module and
convert scripts/main.sh to scripts/install.sh.tftpl, matching the
pattern used by coder/claude-code and coder-labs/codex.

Changes:
- Replace coder_script.agentapi with module "coder_utils" (v0.0.1)
- Convert scripts/main.sh to scripts/install.sh.tftpl using
  templatefile() for variable injection instead of ARG_* env vars
- Keep coder_script.agentapi_shutdown as-is (coder-utils does not
  support run_on_stop)
- Keep coder_app resources (web and CLI) unchanged
- Add output "scripts" for downstream sync dependencies
- Simplify agentapi.tftest.hcl assertions (install script content is
  now base64-encoded inside the coder-utils wrapper)
This commit is contained in:
Jay Kumar 2026-05-13 04:55:59 +00:00
parent fafe8cca7b
commit b8ae549102
3 changed files with 80 additions and 96 deletions

View File

@ -27,22 +27,6 @@ run "default_values" {
error_message = "pid_file_path should default to empty string" error_message = "pid_file_path should default to empty string"
} }
# Verify start script contains state persistence ARG_ vars.
assert {
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE", coder_script.agentapi.script))
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE"
}
assert {
condition = can(regex("ARG_STATE_FILE_PATH", coder_script.agentapi.script))
error_message = "start script should contain ARG_STATE_FILE_PATH"
}
assert {
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi.script))
error_message = "start script should contain ARG_PID_FILE_PATH"
}
# Verify shutdown script contains PID-related ARG_ vars. # Verify shutdown script contains PID-related ARG_ vars.
assert { assert {
condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi_shutdown.script)) condition = can(regex("ARG_PID_FILE_PATH", coder_script.agentapi_shutdown.script))
@ -67,11 +51,10 @@ run "state_persistence_disabled" {
error_message = "enable_state_persistence should be false" error_message = "enable_state_persistence should be false"
} }
# Even when disabled, the ARG_ vars should still be in the script # Verify shutdown script contains the disabled flag.
# (the shell script handles the conditional logic).
assert { assert {
condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE='false'", coder_script.agentapi.script)) condition = can(regex("ARG_ENABLE_STATE_PERSISTENCE='false'", coder_script.agentapi_shutdown.script))
error_message = "start script should contain ARG_ENABLE_STATE_PERSISTENCE='false'" error_message = "shutdown script should contain ARG_ENABLE_STATE_PERSISTENCE='false'"
} }
} }
@ -83,19 +66,18 @@ run "custom_paths" {
pid_file_path = "/custom/agentapi.pid" pid_file_path = "/custom/agentapi.pid"
} }
assert { # Verify custom paths appear in shutdown script.
condition = can(regex("/custom/state.json", coder_script.agentapi.script))
error_message = "start script should contain custom state_file_path"
}
assert {
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi.script))
error_message = "start script should contain custom pid_file_path"
}
# Verify custom paths also appear in shutdown script.
assert { assert {
condition = can(regex("/custom/agentapi.pid", coder_script.agentapi_shutdown.script)) condition = can(regex("/custom/agentapi.pid", coder_script.agentapi_shutdown.script))
error_message = "shutdown script should contain custom pid_file_path" error_message = "shutdown script should contain custom pid_file_path"
} }
} }
run "scripts_output" {
command = plan
assert {
condition = length(output.scripts) == 1 && output.scripts[0] == "coder-agentapi-install_script"
error_message = "scripts output should list the install script sync name"
}
}

View File

@ -173,8 +173,7 @@ locals {
web_app = var.web_app || local.is_task web_app = var.web_app || local.is_task
# we always trim the slash for consistency # we always trim the slash for consistency
workdir = trimsuffix(var.folder, "/") workdir = trimsuffix(var.folder, "/")
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
// Chat base path is only set if not using a subdomain. // Chat base path is only set if not using a subdomain.
// NOTE: // NOTE:
// - Initial support for --chat-base-path was added in v0.3.1 but configuration // - Initial support for --chat-base-path was added in v0.3.1 but configuration
@ -182,45 +181,38 @@ locals {
// - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID // - As CODER_WORKSPACE_AGENT_NAME is a recent addition we use agent ID
// for backward compatibility. // for backward compatibility.
agentapi_chat_base_path = var.agentapi_subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${var.web_app_slug}/chat" agentapi_chat_base_path = var.agentapi_subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${var.web_app_slug}/chat"
main_script = file("${path.module}/scripts/main.sh")
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh") shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")
lib_script = file("${path.module}/scripts/lib.sh") lib_script = file("${path.module}/scripts/lib.sh")
main_script_destination = "${var.module_directory}/main.sh"
lib_script_destination = "${var.module_directory}/agentapi-lib.sh"
shutdown_script_destination = "${var.module_directory}/agentapi-shutdown.sh" shutdown_script_destination = "${var.module_directory}/agentapi-shutdown.sh"
lib_script_destination = "${var.module_directory}/agentapi-lib.sh"
install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
ARG_MODULE_DIRECTORY = var.module_directory
ARG_WORKDIR = local.workdir
ARG_INSTALL_AGENTAPI = tostring(var.install_agentapi)
ARG_AGENTAPI_VERSION = var.agentapi_version
ARG_WAIT_FOR_START_SCRIPT = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
ARG_AGENTAPI_PORT = tostring(var.agentapi_port)
ARG_AGENTAPI_CHAT_BASE_PATH = local.agentapi_chat_base_path
ARG_TASK_ID = try(data.coder_task.me.id, "")
ARG_TASK_LOG_SNAPSHOT = tostring(var.task_log_snapshot)
ARG_ENABLE_STATE_PERSISTENCE = tostring(var.enable_state_persistence)
ARG_STATE_FILE_PATH = var.state_file_path
ARG_PID_FILE_PATH = var.pid_file_path
ARG_LIB_SCRIPT = base64encode(local.lib_script)
})
} }
resource "coder_script" "agentapi" { module "coder_utils" {
agent_id = var.agent_id source = "registry.coder.com/coder/coder-utils/coder"
display_name = "Install and start AgentAPI" version = "0.0.1"
icon = var.web_app_icon
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
mkdir -p "${var.module_directory}" agent_id = var.agent_id
echo -n '${base64encode(local.main_script)}' | base64 -d > "${local.main_script_destination}" module_directory = var.module_directory
chmod +x "${local.main_script_destination}" display_name_prefix = "AgentAPI"
echo -n '${base64encode(local.lib_script)}' | base64 -d > "${local.lib_script_destination}" icon = var.web_app_icon
install_script = local.install_script
ARG_MODULE_DIRECTORY='${var.module_directory}' \
ARG_WORKDIR="$(echo -n '${base64encode(local.workdir)}' | base64 -d)" \
ARG_INSTALL_AGENTAPI='${var.install_agentapi}' \
ARG_AGENTAPI_VERSION='${var.agentapi_version}' \
ARG_WAIT_FOR_START_SCRIPT="$(echo -n '${local.agentapi_wait_for_start_script_b64}' | base64 -d)" \
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
ARG_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
ARG_ENABLE_STATE_PERSISTENCE='${var.enable_state_persistence}' \
ARG_STATE_FILE_PATH='${var.state_file_path}' \
ARG_PID_FILE_PATH='${var.pid_file_path}' \
ARG_LIB_SCRIPT_PATH="${local.lib_script_destination}" \
"${local.main_script_destination}"
EOT
run_on_start = true
} }
resource "coder_script" "agentapi_shutdown" { resource "coder_script" "agentapi_shutdown" {
@ -289,3 +281,8 @@ resource "coder_app" "agentapi_cli" {
output "task_app_id" { output "task_app_id" {
value = local.web_app ? coder_app.agentapi_web[0].id : "" value = local.web_app ? coder_app.agentapi_web[0].id : ""
} }
output "scripts" {
description = "Ordered list of coder exp sync names for the coder_script resources this module creates, in run order. Scripts that were not configured are absent from the list."
value = module.coder_utils.scripts
}

View File

@ -3,29 +3,34 @@ set -e
set -x set -x
set -o nounset set -o nounset
MODULE_DIRECTORY="$ARG_MODULE_DIRECTORY"
WORKDIR="$ARG_WORKDIR" MODULE_DIRECTORY='${ARG_MODULE_DIRECTORY}'
INSTALL_AGENTAPI="$ARG_INSTALL_AGENTAPI" WORKDIR='${ARG_WORKDIR}'
AGENTAPI_VERSION="$ARG_AGENTAPI_VERSION" INSTALL_AGENTAPI='${ARG_INSTALL_AGENTAPI}'
WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT" AGENTAPI_VERSION='${ARG_AGENTAPI_VERSION}'
AGENTAPI_PORT="$ARG_AGENTAPI_PORT" WAIT_FOR_START_SCRIPT=$(echo -n '${ARG_WAIT_FOR_START_SCRIPT}' | base64 -d)
AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}" AGENTAPI_PORT='${ARG_AGENTAPI_PORT}'
TASK_ID="${ARG_TASK_ID:-}" AGENTAPI_CHAT_BASE_PATH='${ARG_AGENTAPI_CHAT_BASE_PATH}'
TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}" TASK_ID='${ARG_TASK_ID}'
ENABLE_STATE_PERSISTENCE="${ARG_ENABLE_STATE_PERSISTENCE:-false}" TASK_LOG_SNAPSHOT='${ARG_TASK_LOG_SNAPSHOT}'
STATE_FILE_PATH="${ARG_STATE_FILE_PATH:-}" ENABLE_STATE_PERSISTENCE='${ARG_ENABLE_STATE_PERSISTENCE}'
PID_FILE_PATH="${ARG_PID_FILE_PATH:-}" STATE_FILE_PATH='${ARG_STATE_FILE_PATH}'
LIB_SCRIPT_PATH="$ARG_LIB_SCRIPT_PATH" PID_FILE_PATH='${ARG_PID_FILE_PATH}'
LIB_SCRIPT=$(echo -n '${ARG_LIB_SCRIPT}' | base64 -d)
set +o nounset set +o nounset
# Write and source lib.sh
LIB_SCRIPT_PATH="$${MODULE_DIRECTORY}/agentapi-lib.sh"
echo -n "$LIB_SCRIPT" > "$LIB_SCRIPT_PATH"
# shellcheck source=lib.sh # shellcheck source=lib.sh
source "${LIB_SCRIPT_PATH}" source "$LIB_SCRIPT_PATH"
command_exists() { command_exists() {
command -v "$1" > /dev/null 2>&1 command -v "$1" > /dev/null 2>&1
} }
mkdir -p "${MODULE_DIRECTORY}/scripts" mkdir -p "$${MODULE_DIRECTORY}/scripts"
# Check for jq dependency if task log snapshot is enabled. # Check for jq dependency if task log snapshot is enabled.
if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then
@ -34,14 +39,14 @@ if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then
echo "Install jq to enable log snapshot functionality when the workspace stops." echo "Install jq to enable log snapshot functionality when the workspace stops."
fi fi
fi fi
if [ ! -d "${WORKDIR}" ]; then if [ ! -d "$${WORKDIR}" ]; then
echo "Warning: The specified folder '${WORKDIR}' does not exist." echo "Warning: The specified folder '$${WORKDIR}' does not exist."
echo "Creating the folder..." echo "Creating the folder..."
mkdir -p "${WORKDIR}" mkdir -p "$${WORKDIR}"
echo "Folder created successfully." echo "Folder created successfully."
fi fi
# Install AgentAPI if enabled # Install AgentAPI if enabled
if [ "${INSTALL_AGENTAPI}" = "true" ]; then if [ "$${INSTALL_AGENTAPI}" = "true" ]; then
echo "Installing AgentAPI..." echo "Installing AgentAPI..."
arch=$(uname -m) arch=$(uname -m)
if [ "$arch" = "x86_64" ]; then if [ "$arch" = "x86_64" ]; then
@ -52,12 +57,12 @@ if [ "${INSTALL_AGENTAPI}" = "true" ]; then
echo "Error: Unsupported architecture: $arch" echo "Error: Unsupported architecture: $arch"
exit 1 exit 1
fi fi
if [ "${AGENTAPI_VERSION}" = "latest" ]; then if [ "$${AGENTAPI_VERSION}" = "latest" ]; then
# for the latest release the download URL pattern is different than for tagged releases # for the latest release the download URL pattern is different than for tagged releases
# https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases # https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases
download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name" download_url="https://github.com/coder/agentapi/releases/latest/download/$binary_name"
else else
download_url="https://github.com/coder/agentapi/releases/download/${AGENTAPI_VERSION}/$binary_name" download_url="https://github.com/coder/agentapi/releases/download/$${AGENTAPI_VERSION}/$binary_name"
fi fi
curl \ curl \
--retry 5 \ --retry 5 \
@ -76,30 +81,30 @@ if ! command_exists agentapi; then
exit 1 exit 1
fi fi
echo -n "${WAIT_FOR_START_SCRIPT}" > "${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" echo -n "$${WAIT_FOR_START_SCRIPT}" > "$${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh"
chmod +x "${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" chmod +x "$${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh"
export LANG=en_US.UTF-8 export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8 export LC_ALL=en_US.UTF-8
cd "${WORKDIR}" cd "$${WORKDIR}"
export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}" export AGENTAPI_CHAT_BASE_PATH="$${AGENTAPI_CHAT_BASE_PATH:-}"
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation) # Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
export AGENTAPI_ALLOWED_HOSTS="*" export AGENTAPI_ALLOWED_HOSTS="*"
export AGENTAPI_PID_FILE="${PID_FILE_PATH:-${MODULE_DIRECTORY}/agentapi.pid}" export AGENTAPI_PID_FILE="$${PID_FILE_PATH:-$${MODULE_DIRECTORY}/agentapi.pid}"
# Only set state env vars when persistence is enabled and the binary supports # Only set state env vars when persistence is enabled and the binary supports
# it. State persistence requires agentapi >= v0.12.0. # it. State persistence requires agentapi >= v0.12.0.
if [ "${ENABLE_STATE_PERSISTENCE}" = "true" ]; then if [ "$${ENABLE_STATE_PERSISTENCE}" = "true" ]; then
actual_version=$(agentapi_version) actual_version=$(agentapi_version)
if version_at_least 0.12.0 "$actual_version"; then if version_at_least 0.12.0 "$actual_version"; then
export AGENTAPI_STATE_FILE="${STATE_FILE_PATH:-${MODULE_DIRECTORY}/agentapi-state.json}" export AGENTAPI_STATE_FILE="$${STATE_FILE_PATH:-$${MODULE_DIRECTORY}/agentapi-state.json}"
export AGENTAPI_SAVE_STATE="true" export AGENTAPI_SAVE_STATE="true"
export AGENTAPI_LOAD_STATE="true" export AGENTAPI_LOAD_STATE="true"
else else
echo "Warning: State persistence requires agentapi >= v0.12.0 (current: ${actual_version:-unknown}), skipping." echo "Warning: State persistence requires agentapi >= v0.12.0 (current: $${actual_version:-unknown}), skipping."
fi fi
fi fi
nohup "${MODULE_DIRECTORY}/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &> "${MODULE_DIRECTORY}/agentapi-start.log" & nohup "$${MODULE_DIRECTORY}/scripts/agentapi-start.sh" true "$${AGENTAPI_PORT}" &> "$${MODULE_DIRECTORY}/agentapi-start.log" &
"${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}" "$${MODULE_DIRECTORY}/scripts/agentapi-wait-for-start.sh" "$${AGENTAPI_PORT}"