mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bea8eeb34 | |||
| b60ea7c583 | |||
| d425df477c | |||
| 05c3b226e1 | |||
| 2cfbe5f69c | |||
| 186e0c4de6 | |||
| 69e5dc5c80 | |||
| b143b7d9ba | |||
| 0a8930d60d | |||
| d21db0d220 | |||
| 392f6b120a | |||
| 7de72fc7cc | |||
| 3e1ddbf624 | |||
| 0021a9fe7d |
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.39.2
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.3 MiB |
@@ -12,12 +12,12 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
|
||||
install_sourcegraph_amp = true
|
||||
agentapi_version = "2.0.2"
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key
|
||||
install_amp = true
|
||||
agentapi_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ variable "amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
amp_version = "2.0.2"
|
||||
amp_version = "2.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -55,7 +55,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.1"
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
@@ -160,6 +160,16 @@ variable "mcp" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "mode" {
|
||||
type = string
|
||||
description = "Set the agent mode (free, rush, smart) — controls the model, system prompt, and tool selection. Default: smart"
|
||||
default = "smart"
|
||||
validation {
|
||||
condition = contains(["", "free", "rush", "smart"], var.mode)
|
||||
error_message = "Invalid mode. Select one from (free, rush, smart)"
|
||||
}
|
||||
}
|
||||
|
||||
data "external" "env" {
|
||||
program = ["sh", "-c", "echo '{\"CODER_AGENT_TOKEN\":\"'$CODER_AGENT_TOKEN'\",\"CODER_AGENT_URL\":\"'$CODER_AGENT_URL'\"}'"]
|
||||
}
|
||||
@@ -170,6 +180,7 @@ locals {
|
||||
default_base_config = jsonencode({
|
||||
"amp.anthropic.thinking.enabled" = true
|
||||
"amp.todos.enabled" = true
|
||||
"amp.terminal.animation" = false
|
||||
})
|
||||
|
||||
user_config = jsondecode(var.base_amp_config != "" ? var.base_amp_config : local.default_base_config)
|
||||
@@ -237,6 +248,7 @@ module "agentapi" {
|
||||
ARG_AMP_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_AMP_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_MODE='${var.mode}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ANSI colors
|
||||
|
||||
@@ -29,6 +29,7 @@ echo "--------------------------------"
|
||||
printf "Workspace: %s\n" "$ARG_AMP_START_DIRECTORY"
|
||||
printf "Task Prompt: %s\n" "$ARG_AMP_TASK_PROMPT"
|
||||
printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS"
|
||||
printf "ARG_MODE: %s\n" "$ARG_MODE"
|
||||
echo "--------------------------------"
|
||||
|
||||
ensure_command amp
|
||||
@@ -50,6 +51,13 @@ else
|
||||
printf "amp_api_key not provided\n"
|
||||
fi
|
||||
|
||||
ARGS=()
|
||||
|
||||
if [ -n "$ARG_MODE" ]; then
|
||||
printf "Running agent in: %s mode" "$ARG_MODE"
|
||||
ARGS+=(--mode "$ARG_MODE")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
printf "amp task prompt provided : %s" "$ARG_AMP_TASK_PROMPT\n"
|
||||
@@ -58,8 +66,8 @@ if [ -n "$ARG_AMP_TASK_PROMPT" ]; then
|
||||
PROMPT="$ARG_AMP_TASK_PROMPT"
|
||||
fi
|
||||
# Pipe the prompt into amp, which will be run inside agentapi
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- bash -c "echo \"$PROMPT\" | amp" "${ARGS[@]}"
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp
|
||||
agentapi server --type amp --term-width=67 --term-height=1190 -- amp "${ARGS[@]}"
|
||||
fi
|
||||
|
||||
@@ -32,8 +32,8 @@ data "coder_task" "me" {}
|
||||
# Or use a custom agent:
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.0.0"
|
||||
source = "/Users/zach/src/registry/registry/coder/modules/claude-code"
|
||||
# version = "4.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/projects"
|
||||
order = 999
|
||||
@@ -43,6 +43,12 @@ module "claude-code" {
|
||||
model = "sonnet"
|
||||
permission_mode = "plan"
|
||||
post_install_script = data.coder_parameter.setup_script.value
|
||||
enable_boundary = true
|
||||
boundary_version = "v0.2.1"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "DEBUG"
|
||||
boundary_additional_allowed_urls = ["method=GET domain=google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
@@ -359,6 +365,10 @@ resource "docker_container" "workspace" {
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
capabilities {
|
||||
add = ["NET_ADMIN", "SYS_ADMIN"]
|
||||
}
|
||||
security_opts = ["seccomp=unconfined"]
|
||||
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
@@ -0,0 +1,67 @@
|
||||
---
|
||||
display_name: Antigravity
|
||||
description: Add a one-click button to launch Google Antigravity
|
||||
icon: ../../../../.icons/antigravity.svg
|
||||
verified: true
|
||||
tags: [ide, antigravity, ai, google]
|
||||
---
|
||||
|
||||
# Antigravity IDE
|
||||
|
||||
Add a button to open any workspace with a single click in [Antigravity IDE](https://antigravity.google).
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Configure MCP servers for Antigravity
|
||||
|
||||
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.gemini/antigravity/mcp_config.json` using a `coder_script` on workspace start.
|
||||
|
||||
The following example configures Antigravity to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
"github" : {
|
||||
"url" : "https://api.githubcopilot.com/mcp/",
|
||||
"headers" : {
|
||||
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
|
||||
},
|
||||
"type" : "http"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
} from "~test";
|
||||
|
||||
describe("antigravity", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "false",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.gemini/antigravity/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const mcp = JSON.stringify({
|
||||
servers: { demo: { url: "http://localhost:1234" } },
|
||||
});
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
mcp,
|
||||
});
|
||||
const script = findResourceInstance(
|
||||
state,
|
||||
"coder_script",
|
||||
"antigravity_mcp",
|
||||
).script;
|
||||
const resp = await execContainer(id, ["sh", "-c", script]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(
|
||||
id,
|
||||
"/root/.gemini/antigravity/mcp_config.json",
|
||||
);
|
||||
expect(content).toBe(mcp);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Antigravity IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "antigravity"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Antigravity IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Antigravity. When set, writes ~/.gemini/antigravity/mcp_config.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
|
||||
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
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "antigravity"
|
||||
}
|
||||
|
||||
resource "coder_script" "antigravity_mcp" {
|
||||
count = var.mcp != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Antigravity MCP"
|
||||
icon = "/icon/antigravity.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p "$HOME/.gemini/antigravity"
|
||||
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
chmod 600 "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
EOT
|
||||
}
|
||||
|
||||
output "antigravity_url" {
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Antigravity IDE URL."
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
|
||||
ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
|
||||
ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false}
|
||||
ARG_CODER_HOST=${ARG_CODER_HOST:-}
|
||||
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS=${ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS:-}
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
@@ -68,6 +69,12 @@ function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
|
||||
|
||||
# Clean up existing directory if it exists
|
||||
if [ -d "boundary" ]; then
|
||||
rm -rf boundary
|
||||
fi
|
||||
|
||||
git clone https://github.com/coder/boundary.git
|
||||
cd boundary
|
||||
git checkout "$ARG_BOUNDARY_VERSION"
|
||||
@@ -75,14 +82,16 @@ function install_boundary() {
|
||||
# Build the binary
|
||||
make build
|
||||
|
||||
# Install binary and wrapper script (optional)
|
||||
sudo cp boundary /usr/local/bin/
|
||||
sudo cp scripts/boundary-wrapper.sh /usr/local/bin/boundary-run
|
||||
sudo chmod +x /usr/local/bin/boundary-run
|
||||
# Install binary to user-local bin (no sudo needed for simple mode)
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
cp boundary "$HOME/.local/bin/"
|
||||
chmod +x "$HOME/.local/bin/boundary"
|
||||
|
||||
cd ..
|
||||
else
|
||||
# Install boundary using official install script
|
||||
# Install boundary using official install script to user-local directory
|
||||
echo "Installing boundary using official install script (version: $ARG_BOUNDARY_VERSION)"
|
||||
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | bash -s -- --version "$ARG_BOUNDARY_VERSION"
|
||||
curl -fsSL https://raw.githubusercontent.com/coder/boundary/main/install.sh | INSTALL_DIR="$HOME/.local/bin" bash -s -- --version "$ARG_BOUNDARY_VERSION"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -210,12 +219,22 @@ function start_agentapi() {
|
||||
install_boundary
|
||||
|
||||
mkdir -p "$ARG_BOUNDARY_LOG_DIR"
|
||||
printf "Starting with coder boundary enabled\n"
|
||||
printf "Starting with coder boundary enabled (simple mode - no special permissions)\n"
|
||||
|
||||
# Build boundary args with conditional --unprivileged flag
|
||||
BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR")
|
||||
# Build boundary args - using --simple mode (no sudo/capabilities required)
|
||||
BOUNDARY_ARGS=(--simple --log-dir "$ARG_BOUNDARY_LOG_DIR")
|
||||
|
||||
# Add audit socket for reporting to Coder agent
|
||||
AUDIT_SOCKET="${TMPDIR:-/tmp}/coder-boundary-audit.sock"
|
||||
if [ -S "$AUDIT_SOCKET" ]; then
|
||||
BOUNDARY_ARGS+=(--audit-socket "$AUDIT_SOCKET")
|
||||
printf "Using audit socket: %s\n" "$AUDIT_SOCKET"
|
||||
else
|
||||
printf "Warning: Audit socket not found at %s - boundary logs won't be reported to Coder\n" "$AUDIT_SOCKET"
|
||||
fi
|
||||
|
||||
# Add default allowed URLs
|
||||
BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST")
|
||||
BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=${ARG_CODER_HOST%%:*}")
|
||||
|
||||
# Add any additional allowed URLs from the variable
|
||||
if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then
|
||||
@@ -238,8 +257,9 @@ function start_agentapi() {
|
||||
BOUNDARY_ARGS+=(--pprof-port "$ARG_BOUNDARY_PPROF_PORT")
|
||||
fi
|
||||
|
||||
# Use boundary directly with --simple flag (no boundary-run wrapper needed)
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- \
|
||||
boundary-run "${BOUNDARY_ARGS[@]}" -- \
|
||||
boundary "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${ARGS[@]}"
|
||||
else
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
|
||||
@@ -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.3.3"
|
||||
version = "1.4.0"
|
||||
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.3.3"
|
||||
version = "1.4.0"
|
||||
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.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -58,6 +58,7 @@ module "cursor" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("cursor", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -76,21 +79,6 @@ describe("cursor", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.cursor/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -64,26 +64,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/cursor.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"cursor://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/cursor.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
|
||||
protocol = "cursor"
|
||||
}
|
||||
|
||||
resource "coder_script" "cursor_mcp" {
|
||||
@@ -103,6 +98,6 @@ resource "coder_script" "cursor_mcp" {
|
||||
}
|
||||
|
||||
output "cursor_url" {
|
||||
value = coder_app.cursor.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Cursor IDE Desktop URL."
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
# Test for jfrog-oauth module
|
||||
|
||||
run "test_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token for basic test
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run "test_empty_access_token_fails" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with empty access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = ""
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.jfrog
|
||||
]
|
||||
}
|
||||
|
||||
run "test_valid_access_token_succeeds" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the script resource is created
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
|
||||
error_message = "coder_script agent_id should match the input variable"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.display_name == "jfrog"
|
||||
error_message = "coder_script display_name should be 'jfrog'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_jfrog_url_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "invalid-url"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.jfrog_url
|
||||
]
|
||||
}
|
||||
|
||||
run "test_username_field_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "invalid"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.username_field
|
||||
]
|
||||
}
|
||||
|
||||
run "test_with_npm_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
npm = ["global", "@foo:foo", "@bar:bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.run_on_start == true
|
||||
error_message = "coder_script should run on start"
|
||||
}
|
||||
|
||||
# Verify npm configuration is in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf npmc command for npm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
|
||||
error_message = "script should contain scoped npm registry for @foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
|
||||
error_message = "script should contain scoped npm registry for @bar"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_configure_code_server" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
configure_code_server = true
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When configure_code_server is true, env vars should be created
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_url) == 1
|
||||
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
|
||||
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_go_proxy_env" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
go = ["foo", "bar", "baz"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When go package manager is configured, GOPROXY env should be set
|
||||
assert {
|
||||
condition = length(resource.coder_env.goproxy) == 1
|
||||
error_message = "coder_env.goproxy should be created when go package manager is configured"
|
||||
}
|
||||
|
||||
# Verify GOPROXY contains all repos
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
|
||||
error_message = "GOPROXY should contain foo repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
|
||||
error_message = "GOPROXY should contain bar repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
|
||||
error_message = "GOPROXY should contain baz repo"
|
||||
}
|
||||
|
||||
# Verify script contains go configuration
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
|
||||
error_message = "script should contain jf goc command"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_pypi_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
pypi = ["global", "foo", "bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify pip configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf pipc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
|
||||
error_message = "script should contain pip index-url configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
|
||||
error_message = "script should contain extra-index-url for additional repos"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_docker_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify docker registration commands in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for foo.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for bar.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for baz.jfrog.io"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_conda_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
conda = ["conda-main", "conda-secondary", "conda-local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify conda configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
|
||||
error_message = "script should contain conda channels configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
|
||||
error_message = "script should contain conda-main channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
|
||||
error_message = "script should contain conda-secondary channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
|
||||
error_message = "script should contain conda-local channel"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_maven_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
maven = ["central", "snapshots", "local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify maven jf mvnc command
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
|
||||
error_message = "script should contain jf mvnc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
|
||||
error_message = "script should contain repo-resolve-releases for central"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
|
||||
error_message = "script should contain repo-resolve-snapshots for central"
|
||||
}
|
||||
|
||||
# Verify settings.xml content
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
|
||||
error_message = "script should contain maven servers configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
|
||||
error_message = "script should contain central server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
|
||||
error_message = "script should contain snapshots server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
|
||||
error_message = "script should contain local server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
|
||||
error_message = "script should contain central repository URL"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformInit,
|
||||
runTerraformApply,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jfrog-oauth", async () => {
|
||||
type TestVariables = {
|
||||
agent_id: string;
|
||||
jfrog_url: string;
|
||||
package_managers: string;
|
||||
|
||||
username_field?: string;
|
||||
jfrog_server_id?: string;
|
||||
external_auth_id?: string;
|
||||
configure_code_server?: boolean;
|
||||
};
|
||||
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
npm: ["global", "@foo:foo", "@bar:bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const npmrcStanza = `cat << EOF > ~/.npmrc
|
||||
email=${user}@example.com
|
||||
registry=http://${fakeFrogApi}/npm/global
|
||||
//${fakeFrogApi}/npm/global/:_authToken=
|
||||
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||
//${fakeFrogApi}/npm/foo/:_authToken=
|
||||
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||
//${fakeFrogApi}/npm/bar/:_authToken=
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(npmrcStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf npmc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured npm',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a pip config with extra-indexes", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
pypi: ["global", "foo", "bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const pipStanza = `cat << EOF > ~/.pip/pip.conf
|
||||
[global]
|
||||
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
|
||||
extra-index-url =
|
||||
https://${user}:@${fakeFrogApi}/pypi/foo/simple
|
||||
https://${user}:@${fakeFrogApi}/pypi/bar/simple
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(pipStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf pipc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured pypi',
|
||||
);
|
||||
});
|
||||
|
||||
it("registers multiple docker repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const dockerStanza = ["foo", "bar", "baz"]
|
||||
.map((r) => `register_docker "${r}.jfrog.io"`)
|
||||
.join("\n");
|
||||
expect(coderScript.script).toContain(dockerStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured docker',
|
||||
);
|
||||
});
|
||||
|
||||
it("sets goproxy with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
go: ["foo", "bar", "baz"],
|
||||
}),
|
||||
});
|
||||
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||
const proxies = ["foo", "bar", "baz"]
|
||||
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
|
||||
.join(",");
|
||||
expect(proxyEnv.value).toEqual(proxies);
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
expect(coderScript.script).toContain(
|
||||
'jf goc --global --repo-resolve "foo"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured go',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a conda config with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
conda: ["conda-main", "conda-secondary", "conda-local"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const condaStanza = `cat << EOF > ~/.condarc
|
||||
channels:
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-main
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-local
|
||||
- defaults
|
||||
ssl_verify: true
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(condaStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain("jf mvnc --global");
|
||||
expect(coderScript.script).toContain('--server-id-resolve="0"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
|
||||
expect(coderScript.script).toContain('--server-id-deploy="0"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/central</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/snapshots</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/local</url>",
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = data.coder_external_auth.jfrog.access_token != ""
|
||||
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_url" {
|
||||
|
||||
@@ -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.1.1"
|
||||
version = "1.2.0"
|
||||
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.1.1"
|
||||
version = "1.2.0"
|
||||
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.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -60,6 +60,7 @@ module "kiro" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ run "default_output" {
|
||||
condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN"
|
||||
error_message = "Default kiro_url must match expected value"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.order == null
|
||||
error_message = "coder_app order must be null by default"
|
||||
}
|
||||
}
|
||||
|
||||
run "adds_folder" {
|
||||
@@ -53,54 +48,6 @@ run "folder_and_open_recent" {
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_slug_display_name" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
slug = "kiro-ai"
|
||||
display_name = "Kiro AI IDE"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.slug == "kiro-ai"
|
||||
error_message = "coder_app slug must be set to kiro-ai"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.display_name == "Kiro AI IDE"
|
||||
error_message = "coder_app display_name must be set to Kiro AI IDE"
|
||||
}
|
||||
}
|
||||
|
||||
run "sets_order" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
order = 5
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.order == 5
|
||||
error_message = "coder_app order must be set to 5"
|
||||
}
|
||||
}
|
||||
|
||||
run "sets_group" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
group = "AI IDEs"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.kiro.group == "AI IDEs"
|
||||
error_message = "coder_app group must be set to AI IDEs"
|
||||
}
|
||||
}
|
||||
|
||||
run "writes_mcp_json" {
|
||||
command = plan
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("kiro", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -55,47 +58,6 @@ describe("kiro", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("custom slug and display_name", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
slug: "kiro-ai",
|
||||
display_name: "Kiro AI IDE",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("kiro-ai");
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Kiro AI IDE");
|
||||
});
|
||||
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "5",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
group: "AI IDEs",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("AI IDEs");
|
||||
});
|
||||
|
||||
it("writes ~/.kiro/settings/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -38,18 +38,6 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "kiro"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Kiro IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Kiro. When set, writes ~/.kiro/settings/mcp.json."
|
||||
@@ -63,26 +51,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "kiro" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/kiro.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"kiro://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/kiro.svg"
|
||||
coder_app_slug = "kiro-ai"
|
||||
coder_app_display_name = "Kiro AI IDE"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "kiro"
|
||||
}
|
||||
|
||||
resource "coder_script" "kiro_mcp" {
|
||||
@@ -102,6 +85,6 @@ resource "coder_script" "kiro_mcp" {
|
||||
}
|
||||
|
||||
output "kiro_url" {
|
||||
value = coder_app.kiro.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Kiro IDE URL."
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,25 @@
|
||||
display_name: mux
|
||||
description: Coding Agent Multiplexer - Run multiple AI agents in parallel
|
||||
icon: ../../../../.icons/mux.svg
|
||||
verified: false
|
||||
verified: true
|
||||
tags: [ai, agents, development, multiplexer]
|
||||
---
|
||||
|
||||
# mux
|
||||
|
||||
Automatically install and run mux in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
|
||||
```tf
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks
|
||||
@@ -35,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -46,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -59,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -73,7 +75,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.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -87,7 +89,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.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
display_name: Vault CLI
|
||||
description: Installs the Hashicorp Vault CLI and optionally configures token authentication
|
||||
icon: ../../../../.icons/vault.svg
|
||||
verified: true
|
||||
tags: [helper, integration, vault, cli]
|
||||
---
|
||||
|
||||
# Vault CLI
|
||||
|
||||
Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures token authentication. This module focuses on CLI installation and can be used standalone or as a base for other authentication methods.
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The following tools are required in the workspace image:
|
||||
|
||||
- **HTTP client**: `curl`, `wget`, or `busybox` (at least one)
|
||||
- **Archive utility**: `unzip` or `busybox` (at least one)
|
||||
- **jq**: Optional but recommended for reliable JSON parsing (falls back to sed if not available)
|
||||
|
||||
## With Token Authentication
|
||||
|
||||
If you have a Vault token, you can provide it to automatically configure authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Installation (CLI Only)
|
||||
|
||||
Install the Vault CLI without any authentication:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### With Specific Version
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Installation Directory
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
}
|
||||
```
|
||||
|
||||
### With Vault Enterprise Namespace
|
||||
|
||||
For Vault Enterprise users who need to specify a namespace:
|
||||
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
```
|
||||
|
||||
## Related Modules
|
||||
|
||||
For more advanced authentication methods, see:
|
||||
|
||||
- [vault-github](https://registry.coder.com/modules/coder/vault-github) - Authenticate with Vault using GitHub tokens
|
||||
- [vault-jwt](https://registry.coder.com/modules/coder/vault-jwt) - Authenticate with Vault using OIDC/JWT
|
||||
|
||||
For simple token-based authentication, see:
|
||||
|
||||
- [vault-token](https://registry.coder.com/modules/coder/vault-token) - Authenticate with Vault using a token
|
||||
@@ -0,0 +1,90 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "vault_addr" {
|
||||
type = string
|
||||
description = "The address of the Vault server."
|
||||
}
|
||||
|
||||
variable "vault_token" {
|
||||
type = string
|
||||
description = "The Vault token to use for authentication. If not provided, only the CLI will be installed."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "install_dir" {
|
||||
type = string
|
||||
description = "The directory to install the Vault CLI to."
|
||||
default = "/usr/local/bin"
|
||||
}
|
||||
|
||||
variable "vault_cli_version" {
|
||||
type = string
|
||||
description = "The version of the Vault CLI to install."
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = var.vault_cli_version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.vault_cli_version))
|
||||
error_message = "vault_cli_version must be either 'latest' or a semantic version (e.g., '1.15.0')."
|
||||
}
|
||||
}
|
||||
|
||||
variable "vault_namespace" {
|
||||
type = string
|
||||
description = "The Vault Enterprise namespace to use. If not provided, no namespace will be configured."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "vault_cli" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Vault CLI"
|
||||
icon = "/icon/vault.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
VAULT_ADDR = var.vault_addr
|
||||
VAULT_TOKEN = var.vault_token
|
||||
INSTALL_DIR = var.install_dir
|
||||
VAULT_CLI_VERSION = var.vault_cli_version
|
||||
})
|
||||
run_on_start = true
|
||||
start_blocks_login = true
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_addr" {
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_ADDR"
|
||||
value = var.vault_addr
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_token" {
|
||||
count = var.vault_token != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_TOKEN"
|
||||
value = var.vault_token
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_namespace" {
|
||||
count = var.vault_namespace != null ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_NAMESPACE"
|
||||
value = var.vault_namespace
|
||||
}
|
||||
|
||||
output "vault_cli_version" {
|
||||
description = "The version of the Vault CLI that was installed."
|
||||
value = var.vault_cli_version
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
|
||||
run "test_vault_cli_without_token" {
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.value == "https://vault.example.com"
|
||||
error_message = "VAULT_ADDR should match the provided vault_addr"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 0
|
||||
error_message = "VAULT_TOKEN should not be set when vault_token is not provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 0
|
||||
error_message = "VAULT_NAMESPACE should not be set when vault_namespace is not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_addr.name == "VAULT_ADDR"
|
||||
error_message = "VAULT_ADDR environment variable should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].name == "VAULT_TOKEN"
|
||||
error_message = "VAULT_TOKEN environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_version" {
|
||||
variables {
|
||||
vault_cli_version = "1.15.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.15.0"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_custom_install_dir" {
|
||||
variables {
|
||||
install_dir = "/custom/install/dir"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.vault_cli.display_name == "Vault CLI"
|
||||
error_message = "Display name should be 'Vault CLI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_invalid_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "invalid-version"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_valid_semver" {
|
||||
variables {
|
||||
vault_cli_version = "1.18.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.vault_cli_version == "1.18.3"
|
||||
error_message = "Vault CLI version output should match the provided version"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_rejects_v_prefix" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
vault_cli_version = "v1.18.3"
|
||||
}
|
||||
|
||||
expect_failures = [var.vault_cli_version]
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_namespace" {
|
||||
variables {
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].name == "VAULT_NAMESPACE"
|
||||
error_message = "VAULT_NAMESPACE environment variable name should be correct"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_vault_cli_with_token_and_namespace" {
|
||||
variables {
|
||||
vault_token = "test-vault-token"
|
||||
vault_namespace = "admin/my-namespace"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_token) == 1
|
||||
error_message = "VAULT_TOKEN should be set when vault_token is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.vault_namespace) == 1
|
||||
error_message = "VAULT_NAMESPACE should be set when vault_namespace is provided"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_token[0].value == "test-vault-token"
|
||||
error_message = "VAULT_TOKEN should match the provided vault_token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.vault_namespace[0].value == "admin/my-namespace"
|
||||
error_message = "VAULT_NAMESPACE should match the provided vault_namespace"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Convert all templated variables to shell variables
|
||||
VAULT_ADDR=${VAULT_ADDR}
|
||||
VAULT_TOKEN=${VAULT_TOKEN}
|
||||
INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
unzip_safe() {
|
||||
if command -v unzip > /dev/null 2>&1; then
|
||||
command unzip "$@"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox unzip "$@"
|
||||
else
|
||||
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
# Get the architecture of the system
|
||||
ARCH=$(uname -m)
|
||||
if [ "$${ARCH}" = "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$${ARCH}" = "aarch64" ]; then
|
||||
ARCH="arm64"
|
||||
else
|
||||
printf "Unsupported architecture: %s\n" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine OS and validate
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$${OS}" != "linux" ] && [ "$${OS}" != "darwin" ]; then
|
||||
printf "Unsupported OS: %s. Only linux and darwin are supported.\n" "$${OS}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Fetch release information from HashiCorp API
|
||||
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/latest"
|
||||
else
|
||||
API_URL="https://api.releases.hashicorp.com/v1/releases/vault/$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
|
||||
API_RESPONSE=$(fetch "$${API_URL}")
|
||||
if [ -z "$${API_RESPONSE}" ]; then
|
||||
printf "Failed to fetch release information from HashiCorp API.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse version and download URL from API response
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | jq -r '.version')
|
||||
DOWNLOAD_URL=$(printf '%s' "$${API_RESPONSE}" | jq -r --arg os "$${OS}" --arg arch "$${ARCH}" '.builds[] | select(.os == $os and .arch == $arch) | .url')
|
||||
else
|
||||
VAULT_CLI_VERSION=$(printf '%s' "$${API_RESPONSE}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
||||
# Fallback: construct URL manually if jq not available
|
||||
DOWNLOAD_URL="https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_$${OS}_$${ARCH}.zip"
|
||||
fi
|
||||
|
||||
if [ -z "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Failed to determine Vault version.\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$${DOWNLOAD_URL}" ]; then
|
||||
printf "Failed to determine download URL for Vault %s (%s/%s).\n" "$${VAULT_CLI_VERSION}" "$${OS}" "$${ARCH}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Vault version: %s\n" "$${VAULT_CLI_VERSION}"
|
||||
|
||||
# Check if the vault CLI is installed and has the correct version
|
||||
installation_needed=1
|
||||
if command -v vault > /dev/null 2>&1; then
|
||||
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||
installation_needed=0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$${installation_needed}" = "1" ]; then
|
||||
# Download and install Vault
|
||||
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||
printf "Installing Vault CLI ...\n\n"
|
||||
else
|
||||
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VAULT_CLI_VERSION}"
|
||||
fi
|
||||
|
||||
# Create temporary directory for download
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
if ! unzip_safe vault.zip; then
|
||||
printf "Failed to unzip Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Install to the specified directory
|
||||
if [ -n "$${INSTALL_DIR}" ] && [ -w "$${INSTALL_DIR}" ]; then
|
||||
mv vault "$${INSTALL_DIR}/vault"
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
elif [ -n "$${INSTALL_DIR}" ] && [ ! -w "$${INSTALL_DIR}" ]; then
|
||||
# Try with sudo if install dir specified but not writable
|
||||
if sudo mv vault "$${INSTALL_DIR}/vault" 2> /dev/null; then
|
||||
printf "Vault installed to %s successfully!\n\n" "$${INSTALL_DIR}"
|
||||
else
|
||||
printf "Warning: Cannot write to %s. " "$${INSTALL_DIR}"
|
||||
mkdir -p ~/.local/bin
|
||||
if mv vault ~/.local/bin/vault; then
|
||||
printf "Installed to ~/.local/bin instead.\n"
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
else
|
||||
printf "Failed to install Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
elif sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||
printf "Vault installed successfully!\n\n"
|
||||
else
|
||||
mkdir -p ~/.local/bin
|
||||
if ! mv vault ~/.local/bin/vault; then
|
||||
printf "Failed to move Vault to local bin.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
fi
|
||||
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||
fi
|
||||
|
||||
# Clean up temp directory
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run installation
|
||||
if ! install; then
|
||||
printf "Failed to install Vault CLI.\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Indicate token configuration status
|
||||
if [ -n "$${VAULT_TOKEN}" ]; then
|
||||
printf "Vault token has been configured via VAULT_TOKEN environment variable.\n"
|
||||
else
|
||||
printf "No Vault token provided. Use 'vault login' or set VAULT_TOKEN to authenticate.\n"
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
display_name: VSCode Desktop Core
|
||||
display_name: Coder VSCode Desktop Core
|
||||
description: Building block for modules that need to link to an external VSCode-based IDE
|
||||
icon: ../../../../.icons/coder.svg
|
||||
verified: true
|
||||
@@ -11,20 +11,20 @@ tags: [internal, library]
|
||||
> [!CAUTION]
|
||||
> We do not recommend using this module directly. Instead, please consider using one of our [Desktop IDE modules](https://registry.coder.com/modules?search=tag%3Aide).
|
||||
|
||||
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily to be used as a library to create modules for VSCode-based IDEs.
|
||||
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily for internal use by Coder to create modules for VSCode-based IDEs.
|
||||
|
||||
```tf
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -10,9 +10,11 @@ const appName = "vscode-desktop";
|
||||
|
||||
const defaultVariables = {
|
||||
agent_id: "foo",
|
||||
coder_app_icon: "/icon/code.svg",
|
||||
coder_app_slug: "vscode",
|
||||
coder_app_display_name: "VS Code Desktop",
|
||||
|
||||
web_app_icon: "/icon/code.svg",
|
||||
web_app_slug: "vscode",
|
||||
web_app_display_name: "VS Code Desktop",
|
||||
|
||||
protocol: "vscode",
|
||||
};
|
||||
|
||||
@@ -21,80 +23,115 @@ describe("vscode-desktop-core", async () => {
|
||||
|
||||
testRequiredVariables(import.meta.dir, defaultVariables);
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, defaultVariables);
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
describe("coder_app", () => {
|
||||
describe("IDE URI attributes", () => {
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(
|
||||
import.meta.dir,
|
||||
defaultVariables,
|
||||
);
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
|
||||
...defaultVariables,
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
it("sets custom slug and display_name", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, defaultVariables);
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_app_order: "22",
|
||||
...defaultVariables,
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe(
|
||||
defaultVariables.web_app_slug,
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe(
|
||||
defaultVariables.web_app_display_name,
|
||||
);
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_order: "5",
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_group: "web-app-group",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("web-app-group");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,31 +28,31 @@ variable "open_recent" {
|
||||
|
||||
variable "protocol" {
|
||||
type = string
|
||||
description = "The URI protocol for the IDE."
|
||||
description = "The URI protocol the IDE."
|
||||
}
|
||||
|
||||
variable "coder_app_icon" {
|
||||
variable "web_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_slug" {
|
||||
variable "web_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_display_name" {
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_order" {
|
||||
variable "web_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_app_group" {
|
||||
variable "web_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
@@ -65,25 +65,38 @@ resource "coder_app" "vscode-desktop" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
|
||||
icon = var.coder_app_icon
|
||||
slug = var.coder_app_slug
|
||||
display_name = var.coder_app_display_name
|
||||
icon = var.web_app_icon
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
|
||||
order = var.coder_app_order
|
||||
group = var.coder_app_group
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
|
||||
# While the call to "join" is not strictly necessary, it makes the URL more readable.
|
||||
url = join("", [
|
||||
"${var.protocol}://coder.coder-remote/open",
|
||||
var.protocol,
|
||||
"://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
|
||||
/*
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=${data.coder_workspace_owner.me.name}",
|
||||
"&workspace=${data.coder_workspace.me.name}",
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=${data.coder_workspace.me.access_url}",
|
||||
# NOTE: There is a protocol whitelist for the token replacement, so this will only work with the protocols hardcoded in the front-end.
|
||||
# (https://github.com/coder/coder/blob/6ba4b5bbc95e2e528d7f5b1e31fffa200ae1a6db/site/src/modules/apps/apps.ts#L18)
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
*/
|
||||
}
|
||||
|
||||
output "ide_uri" {
|
||||
|
||||
@@ -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.1.2"
|
||||
version = "1.2.0"
|
||||
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.1.2"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ describe("vscode-desktop", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -71,19 +74,4 @@ describe("vscode-desktop", async () => {
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,33 +38,24 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
resource "coder_app" "vscode" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/code.svg"
|
||||
slug = "vscode"
|
||||
display_name = "VS Code Desktop"
|
||||
order = var.order
|
||||
group = var.group
|
||||
agent_id = var.agent_id
|
||||
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
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
|
||||
protocol = "vscode"
|
||||
}
|
||||
|
||||
output "vscode_url" {
|
||||
value = coder_app.vscode.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "VS Code Desktop URL."
|
||||
}
|
||||
}
|
||||
@@ -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.2.1"
|
||||
version = "1.3.0"
|
||||
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.2.1"
|
||||
version = "1.3.0"
|
||||
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.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -58,6 +58,7 @@ module "windsurf" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("windsurf", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -76,21 +79,6 @@ describe("windsurf", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: 22,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.codeium/windsurf/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -16,7 +16,7 @@ variable "agent_id" {
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Cursor IDE."
|
||||
description = "The folder to open in Windsurf Editor."
|
||||
default = ""
|
||||
}
|
||||
|
||||
@@ -63,26 +63,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "windsurf" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/windsurf.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"windsurf://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/windsurf.svg"
|
||||
coder_app_slug = "windsurf"
|
||||
coder_app_display_name = "Windsurf Editor"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "windsurf"
|
||||
}
|
||||
|
||||
resource "coder_script" "windsurf_mcp" {
|
||||
@@ -102,6 +97,6 @@ resource "coder_script" "windsurf_mcp" {
|
||||
}
|
||||
|
||||
output "windsurf_url" {
|
||||
value = coder_app.windsurf.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Windsurf Editor URL."
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ describe("positron-desktop", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "positron",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -70,19 +73,4 @@ describe("positron-desktop", async () => {
|
||||
"positron://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "positron",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,6 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
icon_url = "/icon/positron.svg"
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@@ -42,33 +38,39 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "cursor"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Cursor Desktop"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "positron" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = local.icon_url
|
||||
slug = "positron"
|
||||
display_name = "Positron Desktop"
|
||||
order = var.order
|
||||
group = var.group
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
url = join("", [
|
||||
"positron://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/positron.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
|
||||
protocol = "positron"
|
||||
}
|
||||
|
||||
output "positron_url" {
|
||||
value = coder_app.positron.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Positron Desktop URL."
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ type JsonValue =
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
type TerraformStateResource = {
|
||||
module: string;
|
||||
mode: string;
|
||||
type: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
|
||||
Reference in New Issue
Block a user