chore: sync newest module updates to registry (#84)
## Changes made - Copied over all changes to existing modules, making sure to preserve all relative path updates made specifically for the Registry repo - Copied over all modules that were created since the last sync (Windsurf, Devcontainers-CLI) - Copied over changes from the `test.ts` file ## Notes - This PR does not cover https://github.com/coder/modules/pull/426, which contains a few changes around updating the Bash scripts and the contributing README file. @f0ssel tagging you so that you're aware, but I'll be taking care of the `CONTRIBUTING.md` file --------- Co-authored-by: M Atif Ali <me@matifali.dev>
This commit is contained in:
parent
7ea1f305af
commit
496b09d93f
2
.icons/devcontainers.svg
Normal file
2
.icons/devcontainers.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_devcontainer</title><circle cx="16" cy="16" r="14" style="fill:#193e63"/><polygon points="10.777 22.742 9.343 21.348 12.729 17.865 9.346 14.417 10.774 13.017 15.525 17.859 10.777 22.742" style="fill:#add1ea"/><polygon points="21.42 19.101 22.854 17.706 19.468 14.224 22.851 10.776 21.423 9.376 16.672 14.218 21.42 19.101" style="fill:#add1ea"/></svg>
|
||||
|
After Width: | Height: | Size: 575 B |
3
.icons/windsurf.svg
Normal file
3
.icons/windsurf.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M897.246 286.869H889.819C850.735 286.808 819.017 318.46 819.017 357.539V515.589C819.017 547.15 792.93 572.716 761.882 572.716C743.436 572.716 725.02 563.433 714.093 547.85L552.673 317.304C539.28 298.16 517.486 286.747 493.895 286.747C457.094 286.747 423.976 318.034 423.976 356.657V515.619C423.976 547.181 398.103 572.746 366.842 572.746C348.335 572.746 329.949 563.463 319.021 547.881L138.395 289.882C134.316 284.038 125.154 286.93 125.154 294.052V431.892C125.154 438.862 127.285 445.619 131.272 451.34L309.037 705.2C319.539 720.204 335.033 731.344 352.9 735.392C397.616 745.557 438.77 711.135 438.77 667.278V508.406C438.77 476.845 464.339 451.279 495.904 451.279H495.995C515.02 451.279 532.857 460.562 543.785 476.145L705.235 706.661C718.659 725.835 739.327 737.218 763.983 737.218C801.606 737.218 833.841 705.9 833.841 667.308V508.376C833.841 476.815 859.41 451.249 890.975 451.249H897.276C901.233 451.249 904.43 448.053 904.43 444.097V294.021C904.43 290.065 901.233 286.869 897.276 286.869H897.246Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -2,6 +2,5 @@
|
||||
|
||||
Publish Coder modules and templates for other developers to use.
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
> This repo is in active development. We needed to make it public for technical reasons, but the user experience of actually navigating through it and contributing will be made much better shortly.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "modules",
|
||||
"name": "registry",
|
||||
"scripts": {
|
||||
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
|
||||
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
|
||||
|
||||
@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@ -25,7 +25,7 @@ module "claude-code" {
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and npm must be installed in your workspace to install Claude Code
|
||||
- `screen` must be installed in your workspace to run Claude Code in the background
|
||||
- Either `screen` or `tmux` must be installed in your workspace to run Claude Code in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
@ -43,7 +43,7 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have `screen` installed to use this.
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
@ -83,14 +83,14 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "0.2.57"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_use_screen = true
|
||||
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
@ -102,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@ -54,12 +54,35 @@ variable "experiment_use_screen" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Claude Code in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
resource "coder_script" "claude_code" {
|
||||
agent_id = var.agent_id
|
||||
@ -74,6 +97,14 @@ resource "coder_script" "claude_code" {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Claude Code if enabled
|
||||
if [ "${var.install_claude_code}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
@ -84,11 +115,52 @@ resource "coder_script" "claude_code" {
|
||||
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
coder exp mcp configure claude-code ${var.folder}
|
||||
fi
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Claude Code in the background with tmux..."
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
|
||||
|
||||
# Send the prompt to the tmux session if needed
|
||||
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
|
||||
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
sleep 5
|
||||
tmux send-keys -t claude-code Enter
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Claude Code in the background..."
|
||||
@ -149,20 +221,27 @@ resource "coder_app" "claude_code" {
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t claude-code 2>/dev/null; then
|
||||
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux attach-session -t claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "claude-code"; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log"
|
||||
echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -xRR claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
claude
|
||||
fi
|
||||
EOT
|
||||
|
||||
@ -15,7 +15,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@ -30,7 +30,7 @@ module "code-server" {
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_version = "4.8.3"
|
||||
}
|
||||
@ -44,7 +44,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
@ -62,7 +62,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@ -79,7 +79,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
@ -95,7 +95,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
use_cached = true
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
@ -108,7 +108,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,6 +122,20 @@ variable "subdomain" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "open_in" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`.
|
||||
`"tab"` opens in a new tab in the same browser window.
|
||||
`"slim-window"` opens a new browser window without navigation controls.
|
||||
EOT
|
||||
default = "slim-window"
|
||||
validation {
|
||||
condition = contains(["tab", "slim-window"], var.open_in)
|
||||
error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'."
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_script" "code-server" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "code-server"
|
||||
@ -166,6 +180,7 @@ resource "coder_app" "code-server" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
open_in = var.open_in
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.port}/healthz"
|
||||
|
||||
22
registry/coder/modules/devcontainers-cli/README.md
Normal file
22
registry/coder/modules/devcontainers-cli/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: devcontainers-cli
|
||||
description: devcontainers-cli module provides an easy way to install @devcontainers/cli into a workspace
|
||||
icon: ../../../../.icons/devcontainers.svg
|
||||
verified: true
|
||||
maintainer_github: coder
|
||||
tags: [devcontainers]
|
||||
---
|
||||
|
||||
# devcontainers-cli
|
||||
|
||||
The devcontainers-cli module provides an easy way to install [`@devcontainers/cli`](https://github.com/devcontainers/cli) into a workspace. It can be used within any workspace as it runs only if
|
||||
@devcontainers/cli is not installed yet.
|
||||
`npm` is required and should be pre-installed in order for the module to work.
|
||||
|
||||
```tf
|
||||
module "devcontainers-cli" {
|
||||
source = "registry.coder.com/modules/devcontainers-cli/coder"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
144
registry/coder/modules/devcontainers-cli/main.test.ts
Normal file
144
registry/coder/modules/devcontainers-cli/main.test.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
execContainer,
|
||||
executeScriptInContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
type TerraformState,
|
||||
} from "~test";
|
||||
|
||||
const executeScriptInContainerWithPackageManager = async (
|
||||
state: TerraformState,
|
||||
image: string,
|
||||
packageManager: string,
|
||||
shell = "sh",
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}> => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
// Install the specified package manager
|
||||
if (packageManager === "npm") {
|
||||
await execContainer(id, [shell, "-c", "apk add nodejs npm"]);
|
||||
} else if (packageManager === "pnpm") {
|
||||
await execContainer(id, [
|
||||
shell,
|
||||
"-c",
|
||||
`wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -`,
|
||||
]);
|
||||
} else if (packageManager === "yarn") {
|
||||
await execContainer(id, [
|
||||
shell,
|
||||
"-c",
|
||||
"apk add nodejs npm && npm install -g yarn",
|
||||
]);
|
||||
}
|
||||
|
||||
const pathResp = await execContainer(id, [shell, "-c", "echo $PATH"]);
|
||||
const path = pathResp.stdout.trim();
|
||||
|
||||
console.log(path);
|
||||
|
||||
const resp = await execContainer(
|
||||
id,
|
||||
[shell, "-c", instance.script],
|
||||
[
|
||||
"--env",
|
||||
"CODER_SCRIPT_BIN_DIR=/tmp/coder-script-data/bin",
|
||||
"--env",
|
||||
`PATH=${path}:/tmp/coder-script-data/bin`,
|
||||
],
|
||||
);
|
||||
const stdout = resp.stdout.trim().split("\n");
|
||||
const stderr = resp.stderr.trim().split("\n");
|
||||
return {
|
||||
exitCode: resp.exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
};
|
||||
|
||||
describe("devcontainers-cli", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
it("misses all package managers", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
const output = await executeScriptInContainer(state, "docker:dind");
|
||||
expect(output.exitCode).toBe(1);
|
||||
expect(output.stderr).toEqual([
|
||||
"ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first.",
|
||||
]);
|
||||
}, 15000);
|
||||
|
||||
it("installs devcontainers-cli with npm", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"docker:dind",
|
||||
"npm",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"Installing @devcontainers/cli using npm...",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
it("installs devcontainers-cli with yarn", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"docker:dind",
|
||||
"yarn",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"Installing @devcontainers/cli using yarn...",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /tmp/coder-script-data/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
it("displays warning if docker is not installed", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"alpine",
|
||||
"npm",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available.",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
});
|
||||
23
registry/coder/modules/devcontainers-cli/main.tf
Normal file
23
registry/coder/modules/devcontainers-cli/main.tf
Normal file
@ -0,0 +1,23 @@
|
||||
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."
|
||||
}
|
||||
|
||||
resource "coder_script" "devcontainers-cli" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "devcontainers-cli"
|
||||
icon = "/icon/devcontainers.svg"
|
||||
script = templatefile("${path.module}/run.sh", {})
|
||||
run_on_start = true
|
||||
}
|
||||
56
registry/coder/modules/devcontainers-cli/run.sh
Normal file
56
registry/coder/modules/devcontainers-cli/run.sh
Normal file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# If @devcontainers/cli is already installed, we can skip
|
||||
if command -v devcontainer >/dev/null 2>&1; then
|
||||
echo "🥳 @devcontainers/cli is already installed into $(which devcontainer)!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if docker is installed
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available."
|
||||
fi
|
||||
|
||||
# Determine the package manager to use: npm, pnpm, or yarn
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="yarn"
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="npm"
|
||||
elif command -v pnpm >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="pnpm"
|
||||
else
|
||||
echo "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install() {
|
||||
echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..."
|
||||
if [ "$PACKAGE_MANAGER" = "npm" ]; then
|
||||
npm install -g @devcontainers/cli
|
||||
elif [ "$PACKAGE_MANAGER" = "pnpm" ]; then
|
||||
# Check if PNPM_HOME is set, if not, set it to the script's bin directory
|
||||
# pnpm needs this to be set to install binaries
|
||||
# coder agent ensures this part is part of the PATH
|
||||
# so that the devcontainer command is available
|
||||
if [ -z "$PNPM_HOME" ]; then
|
||||
PNPM_HOME="$CODER_SCRIPT_BIN_DIR"
|
||||
export M_HOME
|
||||
fi
|
||||
pnpm add -g @devcontainers/cli
|
||||
elif [ "$PACKAGE_MANAGER" = "yarn" ]; then
|
||||
yarn global add @devcontainers/cli --prefix "$(dirname "$CODER_SCRIPT_BIN_DIR")"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! install; then
|
||||
echo "Failed to install @devcontainers/cli" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v devcontainer >/dev/null 2>&1; then
|
||||
echo "Installation completed but 'devcontainer' command not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🥳 @devcontainers/cli has been installed into $(which devcontainer)!"
|
||||
exit 0
|
||||
105
registry/coder/modules/filebrowser/main.test.ts
Normal file
105
registry/coder/modules/filebrowser/main.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
executeScriptInContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
type scriptOutput,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
function testBaseLine(output: scriptOutput) {
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
const expectedLines = [
|
||||
"\u001b[[0;1mInstalling filebrowser ",
|
||||
"🥳 Installation complete! ",
|
||||
"👷 Starting filebrowser in background... ",
|
||||
"📂 Serving /root at http://localhost:13339 ",
|
||||
"📝 Logs at /tmp/filebrowser.log",
|
||||
];
|
||||
|
||||
// we could use expect(output.stdout).toEqual(expect.arrayContaining(expectedLines)), but when it errors, it doesn't say which line is wrong
|
||||
for (const line of expectedLines) {
|
||||
expect(output.stdout).toContain(line);
|
||||
}
|
||||
}
|
||||
|
||||
describe("filebrowser", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("fails with wrong database_path", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
database_path: "nofb",
|
||||
}).catch((e) => {
|
||||
if (!e.message.startsWith("\nError: Invalid value for variable")) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("runs with default", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
|
||||
it("runs with database_path var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
database_path: ".config/filebrowser.db",
|
||||
});
|
||||
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
|
||||
it("runs with folder var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder/project",
|
||||
});
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs with subdomain=false", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "main",
|
||||
subdomain: false,
|
||||
});
|
||||
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
});
|
||||
@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[[0;1m'
|
||||
|
||||
printf "$${BOLD}Installing filebrowser \n\n"
|
||||
|
||||
# Check if filebrowser is installed
|
||||
if ! command -v filebrowser &> /dev/null; then
|
||||
if ! command -v filebrowser &>/dev/null; then
|
||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||
fi
|
||||
|
||||
@ -32,6 +34,6 @@ printf "👷 Starting filebrowser in background... \n\n"
|
||||
|
||||
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
|
||||
|
||||
filebrowser >> ${LOG_PATH} 2>&1 &
|
||||
filebrowser >>${LOG_PATH} 2>&1 &
|
||||
|
||||
printf "📝 Logs at ${LOG_PATH} \n\n"
|
||||
|
||||
@ -14,7 +14,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@ -90,7 +90,7 @@ resource "coder_agent" "main" {
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@ -111,6 +111,36 @@ module "goose" {
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (MCP)
|
||||
|
||||
You can extend Goose's capabilities by adding custom extensions. For example, to add the desktop-commander extension:
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
# ... other configuration ...
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
npm i -g @wonderwhy-er/desktop-commander@latest
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
desktop-commander:
|
||||
args: []
|
||||
cmd: desktop-commander
|
||||
description: Ideal for background tasks
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: desktop-commander
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
This will add the desktop-commander extension to Goose, allowing it to run commands in the background. The extension will be available in the Goose interface and can be used to run long-running processes like development servers.
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
|
||||
@ -118,7 +148,7 @@ Run Goose as a standalone app in your workspace. This will install Goose and run
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@ -78,6 +78,60 @@ variable "experiment_goose_model" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
|
||||
# Add two spaces to each line of extensions to match YAML structure
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
@ -92,6 +146,14 @@ resource "coder_script" "goose" {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Goose if enabled
|
||||
if [ "${var.install_goose}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
@ -102,6 +164,14 @@ resource "coder_script" "goose" {
|
||||
RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
# Configure Goose if auto-configure is enabled
|
||||
if [ "${var.experiment_auto_configure}" = "true" ]; then
|
||||
echo "Configuring Goose..."
|
||||
@ -109,29 +179,14 @@ resource "coder_script" "goose" {
|
||||
cat > "$HOME/.config/goose/config.yaml" << EOL
|
||||
GOOSE_PROVIDER: ${var.experiment_goose_provider}
|
||||
GOOSE_MODEL: ${var.experiment_goose_model}
|
||||
extensions:
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) before and after starting
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Write system prompt to config
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Goose in the background..."
|
||||
@ -162,14 +217,28 @@ EOL
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS goose bash -c '
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
screen -U -dmS goose bash -c "
|
||||
cd ${var.folder}
|
||||
$HOME/.local/bin/goose run --text "$GOOSE_SYSTEM_PROMPT. Your task: $GOOSE_TASK_PROMPT" --interactive | tee -a "$HOME/.goose.log"
|
||||
exec bash
|
||||
'
|
||||
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
|
||||
/bin/bash
|
||||
"
|
||||
else
|
||||
# Check if goose is installed before running
|
||||
if ! command_exists $HOME/.local/bin/goose; then
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
@ -186,21 +255,34 @@ resource "coder_app" "goose" {
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "goose"; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
echo "Attaching to existing Goose session." | tee -a "$HOME/.goose.log"
|
||||
screen -xRR goose
|
||||
else
|
||||
echo "Starting a new Goose session." | tee -a "$HOME/.goose.log"
|
||||
screen -S goose bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; $HOME/.local/bin/goose run --text "Always report status and instructions to Coder, before and after your steps" --interactive | tee -a "$HOME/.goose.log"; exec bash'
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "goose"; then
|
||||
echo "Error: No existing Goose session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
# Only attach to existing session
|
||||
screen -xRR goose
|
||||
else
|
||||
cd ${var.folder}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
$HOME/.local/bin/goose
|
||||
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
|
||||
@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@ -26,7 +26,7 @@ module "jetbrains_gateway" {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
@ -36,7 +36,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@ -50,7 +50,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@ -65,7 +65,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@ -90,7 +90,7 @@ module "jetbrains_gateway" {
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@ -13,6 +13,16 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
variable "arch" {
|
||||
type = string
|
||||
description = "The target architecture of the workspace"
|
||||
default = "amd64"
|
||||
validation {
|
||||
condition = contains(["amd64", "arm64"], var.arch)
|
||||
error_message = "Architecture must be either 'amd64' or 'arm64'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@ -178,78 +188,100 @@ data "http" "jetbrains_ide_versions" {
|
||||
}
|
||||
|
||||
locals {
|
||||
# AMD64 versions of the images just use the version string, while ARM64
|
||||
# versions append "-aarch64". Eg:
|
||||
#
|
||||
# https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz
|
||||
# https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz
|
||||
#
|
||||
# We rewrite the data map above dynamically based on the user's architecture parameter.
|
||||
#
|
||||
effective_jetbrains_ide_versions = {
|
||||
for k, v in var.jetbrains_ide_versions : k => {
|
||||
build_number = v.build_number
|
||||
version = var.arch == "arm64" ? "${v.version}-aarch64" : v.version
|
||||
}
|
||||
}
|
||||
|
||||
# When downloading the latest IDE, the download link in the JSON is either:
|
||||
#
|
||||
# linux.download_link
|
||||
# linuxARM64.download_link
|
||||
#
|
||||
download_key = var.arch == "arm64" ? "linuxARM64" : "linux"
|
||||
|
||||
jetbrains_ides = {
|
||||
"GO" = {
|
||||
icon = "/icon/goland.svg",
|
||||
name = "GoLand",
|
||||
identifier = "GO",
|
||||
build_number = var.jetbrains_ide_versions["GO"].build_number,
|
||||
download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["GO"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["GO"].build_number,
|
||||
download_link = "${var.download_base_link}/go/goland-${local.effective_jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["GO"].version
|
||||
},
|
||||
"WS" = {
|
||||
icon = "/icon/webstorm.svg",
|
||||
name = "WebStorm",
|
||||
identifier = "WS",
|
||||
build_number = var.jetbrains_ide_versions["WS"].build_number,
|
||||
download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["WS"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["WS"].build_number,
|
||||
download_link = "${var.download_base_link}/webstorm/WebStorm-${local.effective_jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["WS"].version
|
||||
},
|
||||
"IU" = {
|
||||
icon = "/icon/intellij.svg",
|
||||
name = "IntelliJ IDEA Ultimate",
|
||||
identifier = "IU",
|
||||
build_number = var.jetbrains_ide_versions["IU"].build_number,
|
||||
download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["IU"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["IU"].build_number,
|
||||
download_link = "${var.download_base_link}/idea/ideaIU-${local.effective_jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["IU"].version
|
||||
},
|
||||
"PY" = {
|
||||
icon = "/icon/pycharm.svg",
|
||||
name = "PyCharm Professional",
|
||||
identifier = "PY",
|
||||
build_number = var.jetbrains_ide_versions["PY"].build_number,
|
||||
download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PY"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["PY"].build_number,
|
||||
download_link = "${var.download_base_link}/python/pycharm-professional-${local.effective_jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["PY"].version
|
||||
},
|
||||
"CL" = {
|
||||
icon = "/icon/clion.svg",
|
||||
name = "CLion",
|
||||
identifier = "CL",
|
||||
build_number = var.jetbrains_ide_versions["CL"].build_number,
|
||||
download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["CL"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["CL"].build_number,
|
||||
download_link = "${var.download_base_link}/cpp/CLion-${local.effective_jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["CL"].version
|
||||
},
|
||||
"PS" = {
|
||||
icon = "/icon/phpstorm.svg",
|
||||
name = "PhpStorm",
|
||||
identifier = "PS",
|
||||
build_number = var.jetbrains_ide_versions["PS"].build_number,
|
||||
download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PS"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["PS"].build_number,
|
||||
download_link = "${var.download_base_link}/webide/PhpStorm-${local.effective_jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["PS"].version
|
||||
},
|
||||
"RM" = {
|
||||
icon = "/icon/rubymine.svg",
|
||||
name = "RubyMine",
|
||||
identifier = "RM",
|
||||
build_number = var.jetbrains_ide_versions["RM"].build_number,
|
||||
download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RM"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RM"].build_number,
|
||||
download_link = "${var.download_base_link}/ruby/RubyMine-${local.effective_jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RM"].version
|
||||
},
|
||||
"RD" = {
|
||||
icon = "/icon/rider.svg",
|
||||
name = "Rider",
|
||||
identifier = "RD",
|
||||
build_number = var.jetbrains_ide_versions["RD"].build_number,
|
||||
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RD"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RD"].build_number,
|
||||
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${local.effective_jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RD"].version
|
||||
},
|
||||
"RR" = {
|
||||
icon = "/icon/rustrover.svg",
|
||||
name = "RustRover",
|
||||
identifier = "RR",
|
||||
build_number = var.jetbrains_ide_versions["RR"].build_number,
|
||||
download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RR"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RR"].build_number,
|
||||
download_link = "${var.download_base_link}/rustrover/RustRover-${local.effective_jetbrains_ide_versions["RR"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RR"].version
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,7 +290,7 @@ locals {
|
||||
key = var.latest ? keys(local.json_data)[0] : ""
|
||||
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
|
||||
identifier = data.coder_parameter.jetbrains_ide.value
|
||||
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
download_link = var.latest ? local.json_data[local.key][0].downloads[local.download_key].link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number
|
||||
version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ A module that adds JupyterLab in your Coder template.
|
||||
module "jupyterlab" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jupyterlab/coder"
|
||||
version = "1.0.30"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@ -3,13 +3,13 @@ INSTALLER=""
|
||||
check_available_installer() {
|
||||
# check if pipx is installed
|
||||
echo "Checking for a supported installer"
|
||||
if command -v pipx > /dev/null 2>&1; then
|
||||
if command -v pipx >/dev/null 2>&1; then
|
||||
echo "pipx is installed"
|
||||
INSTALLER="pipx"
|
||||
return
|
||||
fi
|
||||
# check if uv is installed
|
||||
if command -v uv > /dev/null 2>&1; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
echo "uv is installed"
|
||||
INSTALLER="uv"
|
||||
return
|
||||
@ -26,32 +26,33 @@ fi
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
# check if jupyterlab is installed
|
||||
if ! command -v jupyter-lab > /dev/null 2>&1; then
|
||||
if ! command -v jupyter-lab >/dev/null 2>&1; then
|
||||
# install jupyterlab
|
||||
check_available_installer
|
||||
printf "$${BOLD}Installing jupyterlab!\n"
|
||||
case $INSTALLER in
|
||||
uv)
|
||||
uv pip install -q jupyterlab \
|
||||
&& printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTERPATH="$HOME/.venv/bin/"
|
||||
;;
|
||||
pipx)
|
||||
pipx install jupyterlab \
|
||||
&& printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTERPATH="$HOME/.local/bin"
|
||||
;;
|
||||
uv)
|
||||
uv pip install -q jupyterlab &&
|
||||
printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTER="$HOME/.venv/bin/jupyter-lab"
|
||||
;;
|
||||
pipx)
|
||||
pipx install jupyterlab &&
|
||||
printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTER="$HOME/.local/bin/jupyter-lab"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf "%s\n\n" "🥳 jupyterlab is already installed"
|
||||
JUPYTER=$(command -v jupyter-lab)
|
||||
fi
|
||||
|
||||
printf "👷 Starting jupyterlab in background..."
|
||||
printf "check logs at ${LOG_PATH}"
|
||||
$JUPYTERPATH/jupyter-lab --no-browser \
|
||||
$JUPYTER --no-browser \
|
||||
"$BASE_URL_FLAG" \
|
||||
--ServerApp.ip='*' \
|
||||
--ServerApp.port="${PORT}" \
|
||||
--ServerApp.token='' \
|
||||
--ServerApp.password='' \
|
||||
> "${LOG_PATH}" 2>&1 &
|
||||
>"${LOG_PATH}" 2>&1 &
|
||||
|
||||
@ -10,16 +10,17 @@ tags: [helper, integration, vault, jwt, oidc]
|
||||
|
||||
# Hashicorp Vault Integration (JWT)
|
||||
|
||||
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
|
||||
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method or another source of jwt token. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = "eyJhbGciOiJIUzI1N..." # optional, if not present, defaults to user's oidc authentication token
|
||||
}
|
||||
```
|
||||
|
||||
@ -43,7 +44,7 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_auth_path = "oidc"
|
||||
@ -59,7 +60,7 @@ data "coder_workspace_owner" "me" {}
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
|
||||
@ -72,10 +73,113 @@ module "vault" {
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_cli_version = "1.17.5"
|
||||
}
|
||||
```
|
||||
|
||||
### Use a custom JWT token
|
||||
|
||||
```tf
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
jwt = {
|
||||
source = "geektheripper/jwt"
|
||||
version = "1.1.4"
|
||||
}
|
||||
time = {
|
||||
source = "hashicorp/time"
|
||||
version = "0.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "jwt_signed_token" "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
algorithm = "RS256"
|
||||
# `openssl genrsa -out key.pem 4096` and `openssl rsa -in key.pem -pubout > pub.pem` to generate keys
|
||||
key = file("key.pem")
|
||||
claims_json = jsonencode({
|
||||
iss = "https://code.example.com"
|
||||
sub = "${data.coder_workspace.me.id}"
|
||||
aud = "https://vault.example.com"
|
||||
iat = provider::time::rfc3339_parse(plantimestamp()).unix
|
||||
# Uncomment to set an expiry on the JWT token(default 3600 seconds).
|
||||
# workspace will need to be restarted to generate a new token if it expires
|
||||
#exp = provider::time::rfc3339_parse(timeadd(timestamp(), 3600)).unix agent = coder_agent.main.id
|
||||
provisioner = data.coder_provisioner.main.id
|
||||
provisioner_arch = data.coder_provisioner.main.arch
|
||||
provisioner_os = data.coder_provisioner.main.os
|
||||
|
||||
workspace = data.coder_workspace.me.id
|
||||
workspace_url = data.coder_workspace.me.access_url
|
||||
workspace_port = data.coder_workspace.me.access_port
|
||||
workspace_name = data.coder_workspace.me.name
|
||||
template = data.coder_workspace.me.template_id
|
||||
template_name = data.coder_workspace.me.template_name
|
||||
template_version = data.coder_workspace.me.template_version
|
||||
owner = data.coder_workspace_owner.me.id
|
||||
owner_name = data.coder_workspace_owner.me.name
|
||||
owner_email = data.coder_workspace_owner.me.email
|
||||
owner_login_type = data.coder_workspace_owner.me.login_type
|
||||
owner_groups = data.coder_workspace_owner.me.groups
|
||||
})
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = jwt_signed_token.vault[0].token
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Vault JWT role
|
||||
|
||||
```shell
|
||||
vault write auth/JWT_MOUNT/role/workspace - << EOF
|
||||
{
|
||||
"user_claim": "sub",
|
||||
"bound_audiences": "https://vault.example.com",
|
||||
"role_type": "jwt",
|
||||
"ttl": "1h",
|
||||
"claim_mappings": {
|
||||
"owner": "owner",
|
||||
"owner_email": "owner_email",
|
||||
"owner_login_type": "owner_login_type",
|
||||
"owner_name": "owner_name",
|
||||
"provisioner": "provisioner",
|
||||
"provisioner_arch": "provisioner_arch",
|
||||
"provisioner_os": "provisioner_os",
|
||||
"sub": "sub",
|
||||
"template": "template",
|
||||
"template_name": "template_name",
|
||||
"template_version": "template_version",
|
||||
"workspace": "workspace",
|
||||
"workspace_name": "workspace_name",
|
||||
"workspace_id": "workspace_id"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example workspace access Vault policy
|
||||
|
||||
```tf
|
||||
path "kv/data/app/coder/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.owner_name}}/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.workspace_name}}" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "subscribe"]
|
||||
subscribe_event_types = ["*"]
|
||||
}
|
||||
path "kv/metadata/app/coder/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.owner_name}}/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.workspace_name}}" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "subscribe"]
|
||||
subscribe_event_types = ["*"]
|
||||
}
|
||||
```
|
||||
|
||||
@ -20,6 +20,13 @@ variable "vault_addr" {
|
||||
description = "The address of the Vault server."
|
||||
}
|
||||
|
||||
variable "vault_jwt_token" {
|
||||
type = string
|
||||
description = "The JWT token used for authentication with Vault."
|
||||
default = null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "vault_jwt_auth_path" {
|
||||
type = string
|
||||
description = "The path to the Vault JWT auth method."
|
||||
@ -46,7 +53,7 @@ resource "coder_script" "vault" {
|
||||
display_name = "Vault (GitHub)"
|
||||
icon = "/icon/vault.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token,
|
||||
CODER_OIDC_ACCESS_TOKEN : var.vault_jwt_token != null ? var.vault_jwt_token : data.coder_workspace_owner.me.oidc_access_token,
|
||||
VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path,
|
||||
VAULT_JWT_ROLE : var.vault_jwt_role,
|
||||
VAULT_CLI_VERSION : var.vault_cli_version,
|
||||
|
||||
@ -9,11 +9,11 @@ CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN}
|
||||
fetch() {
|
||||
dest="$1"
|
||||
url="$2"
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -O "$${dest}" "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
elif command -v busybox >/dev/null 2>&1; then
|
||||
busybox wget -O "$${dest}" "$${url}"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
@ -22,9 +22,9 @@ fetch() {
|
||||
}
|
||||
|
||||
unzip_safe() {
|
||||
if command -v unzip > /dev/null 2>&1; then
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
command unzip "$@"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
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"
|
||||
@ -56,7 +56,7 @@ install() {
|
||||
|
||||
# Check if the vault CLI is installed and has the correct version
|
||||
installation_needed=1
|
||||
if command -v vault > /dev/null 2>&1; then
|
||||
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}"
|
||||
@ -81,7 +81,7 @@ install() {
|
||||
return 1
|
||||
fi
|
||||
rm vault.zip
|
||||
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||
if sudo mv vault /usr/local/bin/vault 2>/dev/null; then
|
||||
printf "Vault installed successfully!\n\n"
|
||||
else
|
||||
mkdir -p ~/.local/bin
|
||||
@ -107,6 +107,6 @@ rm -rf "$TMP"
|
||||
|
||||
# Authenticate with Vault
|
||||
printf "🔑 Authenticating with Vault ...\n\n"
|
||||
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=-
|
||||
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write -field=token auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- | vault login -
|
||||
printf "🥳 Vault authentication complete!\n\n"
|
||||
printf "You can now use Vault CLI to access secrets.\n"
|
||||
|
||||
37
registry/coder/modules/windsurf/README.md
Normal file
37
registry/coder/modules/windsurf/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
display_name: Windsurf Editor
|
||||
description: Add a one-click button to launch Windsurf Editor
|
||||
icon: ../../../../.icons/windsurf.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, windsurf, helper, ai]
|
||||
---
|
||||
|
||||
# Windsurf Editor
|
||||
|
||||
Add a button to open any workspace with a single click in Windsurf Editor.
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/windsurf/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/windsurf/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
88
registry/coder/modules/windsurf/main.test.ts
Normal file
88
registry/coder/modules/windsurf/main.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("windsurf", 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.windsurf_url.value).toBe(
|
||||
"windsurf://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 === "windsurf",
|
||||
);
|
||||
|
||||
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.windsurf_url.value).toBe(
|
||||
"windsurf://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.windsurf_url.value).toBe(
|
||||
"windsurf://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.windsurf_url.value).toBe(
|
||||
"windsurf://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.windsurf_url.value).toBe(
|
||||
"windsurf://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 === "windsurf",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
62
registry/coder/modules/windsurf/main.tf
Normal file
62
registry/coder/modules/windsurf/main.tf
Normal file
@ -0,0 +1,62 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Cursor 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
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "windsurf" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/windsurf.svg"
|
||||
slug = "windsurf"
|
||||
display_name = "Windsurf Editor"
|
||||
order = var.order
|
||||
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",
|
||||
])
|
||||
}
|
||||
|
||||
output "windsurf_url" {
|
||||
value = coder_app.windsurf.url
|
||||
description = "Windsurf Editor URL."
|
||||
}
|
||||
@ -23,13 +23,13 @@ describe("exoscale-instance-type", async () => {
|
||||
expect(state.outputs.value.value).toBe("gpu3.huge");
|
||||
});
|
||||
|
||||
it("fails because of wrong categroy definition", async () => {
|
||||
it("fails because of wrong category definition", async () => {
|
||||
expect(async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
default: "gpu3.huge",
|
||||
// type_category: ["standard"] is standard
|
||||
});
|
||||
}).toThrow('default value "gpu3.huge" must be defined as one of options');
|
||||
}).toThrow(/value "gpu3.huge" must be defined as one of options/);
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
|
||||
21
test/test.ts
21
test/test.ts
@ -30,6 +30,12 @@ export const runContainer = async (
|
||||
return containerID.trim();
|
||||
};
|
||||
|
||||
export interface scriptOutput {
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the only "coder_script" resource in the given state and runs it in a
|
||||
* container.
|
||||
@ -38,13 +44,15 @@ export const executeScriptInContainer = async (
|
||||
state: TerraformState,
|
||||
image: string,
|
||||
shell = "sh",
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}> => {
|
||||
before?: string,
|
||||
): Promise<scriptOutput> => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
if (before) {
|
||||
await execContainer(id, [shell, "-c", before]);
|
||||
}
|
||||
|
||||
const resp = await execContainer(id, [shell, "-c", instance.script]);
|
||||
const stdout = resp.stdout.trim().split("\n");
|
||||
const stderr = resp.stderr.trim().split("\n");
|
||||
@ -58,12 +66,13 @@ export const executeScriptInContainer = async (
|
||||
export const execContainer = async (
|
||||
id: string,
|
||||
cmd: string[],
|
||||
args?: string[],
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stderr: string;
|
||||
stdout: string;
|
||||
}> => {
|
||||
const proc = spawn(["docker", "exec", id, ...cmd], {
|
||||
const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], {
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user