mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5f6a00851 | |||
| 05e6324e41 | |||
| fd6f980610 | |||
| 3447d31392 | |||
| 618a9b8b5d | |||
| 847c9491af | |||
| 10142cbe1c | |||
| 8ec817e33c | |||
| 480bf4b48c | |||
| d8851492c0 | |||
| 186a779659 | |||
| 8defcb2410 | |||
| 08bd84c529 | |||
| c493bbd490 | |||
| b55d546f03 | |||
| f1d5947245 | |||
| e1eda2ce65 | |||
| a1eed799aa | |||
| b52c0f9f63 |
Binary file not shown.
|
Before Width: | Height: | Size: 975 KiB |
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -3,20 +3,22 @@ set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
port=${1:-3284}
|
||||
max_attempts=150
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# This script waits for the agentapi server to start on the given port.
|
||||
# Each attempt sleeps 0.1s, so 150 attempts ≈ 15 seconds.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
for i in $(seq 1 "$max_attempts"); do
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
echo "agentapi server not responding ($i/$max_attempts)"
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
@@ -25,7 +27,7 @@ for i in $(seq 1 150); do
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
echo "Error: agentapi server did not start on port $port after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "antigravity" {
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Antigravity to use the GitHub MCP server with a
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -66,15 +66,15 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/antigravity.svg"
|
||||
web_app_slug = var.slug
|
||||
web_app_display_name = var.display_name
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
coder_app_icon = "/icon/antigravity.svg"
|
||||
coder_app_slug = var.slug
|
||||
coder_app_display_name = var.display_name
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -66,7 +66,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -31,7 +31,7 @@ module "kiro" {
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -53,7 +53,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -16,15 +16,15 @@ The VSCode Desktop Core module is a building block for modules that need to expo
|
||||
```tf
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/code.svg"
|
||||
web_app_slug = "vscode"
|
||||
web_app_display_name = "VS Code Desktop"
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
coder_app_icon = "/icon/code.svg"
|
||||
coder_app_slug = "vscode"
|
||||
coder_app_display_name = "VS Code Desktop"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -11,9 +11,9 @@ const appName = "vscode-desktop";
|
||||
const defaultVariables = {
|
||||
agent_id: "foo",
|
||||
|
||||
web_app_icon: "/icon/code.svg",
|
||||
web_app_slug: "vscode",
|
||||
web_app_display_name: "VS Code Desktop",
|
||||
coder_app_icon: "/icon/code.svg",
|
||||
coder_app_slug: "vscode",
|
||||
coder_app_display_name: "VS Code Desktop",
|
||||
|
||||
protocol: "vscode",
|
||||
};
|
||||
@@ -99,16 +99,16 @@ describe("vscode-desktop-core", async () => {
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe(
|
||||
defaultVariables.web_app_slug,
|
||||
defaultVariables.coder_app_slug,
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe(
|
||||
defaultVariables.web_app_display_name,
|
||||
defaultVariables.coder_app_display_name,
|
||||
);
|
||||
});
|
||||
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_order: "5",
|
||||
coder_app_order: "5",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe("vscode-desktop-core", async () => {
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_group: "web-app-group",
|
||||
coder_app_group: "web-app-group",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
@@ -31,28 +31,28 @@ variable "protocol" {
|
||||
description = "The URI protocol the IDE."
|
||||
}
|
||||
|
||||
variable "web_app_icon" {
|
||||
variable "coder_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_slug" {
|
||||
variable "coder_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
variable "coder_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "web_app_order" {
|
||||
variable "coder_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "web_app_group" {
|
||||
variable "coder_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
@@ -65,12 +65,12 @@ resource "coder_app" "vscode-desktop" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
|
||||
icon = var.web_app_icon
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
icon = var.coder_app_icon
|
||||
slug = var.coder_app_slug
|
||||
display_name = var.coder_app_display_name
|
||||
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
order = var.coder_app_order
|
||||
group = var.coder_app_group
|
||||
|
||||
url = join("", [
|
||||
var.protocol,
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "vscode" {
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ variable "group" {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ tags: [ide, vscode, web]
|
||||
|
||||
# VS Code Web
|
||||
|
||||
Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace and create an app to access it via the dashboard.
|
||||
Automatically install the [VS Code CLI](https://code.visualstudio.com/docs/editor/command-line) and run `code serve-web` in a workspace to access VS Code via the browser.
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@@ -30,7 +30,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@@ -44,22 +44,22 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-configure Settings
|
||||
### Pre-configure Machine Settings
|
||||
|
||||
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
|
||||
Configure VS Code's [Machine settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file). These settings are merged with any existing machine settings on startup:
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -69,20 +69,7 @@ module "vscode-web" {
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a specific VS Code Web version
|
||||
|
||||
By default, this module installs the latest. To pin a specific version, retrieve the commit ID from the [VS Code Update API](https://update.code.visualstudio.com/api/commits/stable/server-linux-x64-web) and verify its corresponding release on the [VS Code GitHub Releases](https://github.com/microsoft/vscode/releases).
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
> **Note:** Merging settings requires `jq` or `python3`. If neither is available, existing machine settings will be preserved. User settings configured through the VS Code UI are stored in browser local storage and will not persist across different browsers or devices.
|
||||
|
||||
### Open an existing workspace on startup
|
||||
|
||||
@@ -91,10 +78,43 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
agent_id = coder_agent.example.id
|
||||
workspace = "/home/coder/coder.code-workspace"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workspace = "/home/coder/coder.code-workspace"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Use VS Code Insiders
|
||||
|
||||
Use the VS Code Insiders release channel to get the latest features and bug fixes:
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
release_channel = "insiders"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a specific VS Code version
|
||||
|
||||
Use the `commit_id` variable to pin a specific VS Code Server version by its commit SHA:
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
You can find the commit SHA for a specific VS Code version on the [VS Code releases page](https://code.visualstudio.com/updates) or by checking the "About" dialog in VS Code.
|
||||
|
||||
@@ -1,42 +1,793 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { runTerraformApply, runTerraformInit } from "~test";
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
setDefaultTimeout,
|
||||
} from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
|
||||
// Set timeout to 5 minutes for tests that download VS Code CLI
|
||||
setDefaultTimeout(5 * 60 * 1000);
|
||||
|
||||
let cleanupContainers: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const id of cleanupContainers) {
|
||||
try {
|
||||
await removeContainer(id);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
cleanupContainers = [];
|
||||
});
|
||||
|
||||
describe("vscode-web", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
it("accept_license should be set to true", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "false",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Invalid value for variable");
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
it("use_cached and offline can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
use_cached: "true",
|
||||
offline: "true",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline and Use Cached can not be used together");
|
||||
describe("terraform validation", () => {
|
||||
it("accept_license should be set to true", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: false,
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain("Invalid value for variable");
|
||||
}
|
||||
});
|
||||
|
||||
it("use_cached and offline can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
use_cached: true,
|
||||
offline: true,
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Offline and Use Cached can not be used together",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("offline and extensions can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
extensions: '["ms-python.python"]',
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Offline mode does not allow extensions to be installed",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("workspace and folder can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
folder: "/home/coder",
|
||||
workspace: "/home/coder/test.code-workspace",
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Set only one of `workspace` or `folder`",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("offline and extensions can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
describe("script generation", () => {
|
||||
it("generates script with correct port", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
offline: "true",
|
||||
extensions: '["1", "2"]',
|
||||
accept_license: true,
|
||||
port: 8080,
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline mode does not allow extensions to be installed");
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--port 8080");
|
||||
});
|
||||
|
||||
it("generates script with extensions directory", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
extensions_dir: "/custom/extensions",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--extensions-dir=/custom/extensions");
|
||||
});
|
||||
|
||||
it("generates script with telemetry level", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
telemetry_level: "off",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--telemetry-level off");
|
||||
});
|
||||
|
||||
it("generates script with disable trust", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
disable_trust: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--disable-workspace-trust");
|
||||
});
|
||||
|
||||
it("generates script with serve-web command", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("serve-web");
|
||||
expect(script.script).toContain("--accept-server-license-terms");
|
||||
expect(script.script).toContain("--without-connection-token");
|
||||
});
|
||||
|
||||
it("generates script with stable release channel by default", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("build=stable");
|
||||
});
|
||||
|
||||
it("generates script with insiders release channel", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
release_channel: "insiders",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("build=insiders");
|
||||
});
|
||||
|
||||
it("generates script without commit-id value when not specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
// The if condition should have an empty string, so no commit-id value is passed
|
||||
expect(script.script).toContain('if [ -n "" ]; then');
|
||||
// Should not contain any actual commit hash
|
||||
expect(script.script).not.toMatch(/--commit-id [a-f0-9]{40}/);
|
||||
});
|
||||
|
||||
it("generates script with commit-id when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
commit_id: "e54c774e0add60467559eb0d1e229c6452cf8447",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain(
|
||||
"--commit-id e54c774e0add60467559eb0d1e229c6452cf8447",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// More tests depend on shebang refactors
|
||||
describe("container integration tests", () => {
|
||||
it("uses existing code CLI in PATH", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs when serve-web is called
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_SERVER_STARTED with args: \$@"
|
||||
exit 0
|
||||
fi
|
||||
echo "code mock called: \$@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
// Run the script - the mock will capture the serve-web call
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Found VS Code CLI");
|
||||
});
|
||||
|
||||
it("offline mode fails when CLI not present", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found",
|
||||
);
|
||||
});
|
||||
|
||||
it("offline mode uses code-server as fallback", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install mock code-server in PATH
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code-server << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_SERVER_STARTED with args: $@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code-server`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("offline fallback");
|
||||
expect(result.stdout).toContain("Starting code-server");
|
||||
});
|
||||
|
||||
it("offline mode works with pre-installed CLI", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Pre-install mock code CLI at expected location
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_OFFLINE_SERVER_STARTED"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /tmp/vscode-web/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Using cached VS Code CLI");
|
||||
expect(result.stdout).toContain("Starting VS Code Web");
|
||||
});
|
||||
|
||||
it("use_cached mode works with pre-installed CLI", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
use_cached: true,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Pre-install mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_CACHED_SERVER_STARTED"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /tmp/vscode-web/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Using cached VS Code CLI");
|
||||
});
|
||||
|
||||
it("creates settings file with correct content", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"editor.fontSize": 14}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that settings file was created
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
expect(settingsResult.stdout).toContain("editor.fontSize");
|
||||
expect(settingsResult.stdout).toContain("14");
|
||||
});
|
||||
|
||||
it("creates settings file with multiple settings", async () => {
|
||||
const settings = {
|
||||
"editor.fontSize": 16,
|
||||
"editor.tabSize": 2,
|
||||
"workbench.colorTheme": "Dracula",
|
||||
"editor.formatOnSave": true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that settings file was created with all settings
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
expect(settingsResult.stdout).toContain("editor.fontSize");
|
||||
expect(settingsResult.stdout).toContain("16");
|
||||
expect(settingsResult.stdout).toContain("editor.tabSize");
|
||||
expect(settingsResult.stdout).toContain("2");
|
||||
expect(settingsResult.stdout).toContain("workbench.colorTheme");
|
||||
expect(settingsResult.stdout).toContain("Dracula");
|
||||
expect(settingsResult.stdout).toContain("editor.formatOnSave");
|
||||
expect(settingsResult.stdout).toContain("true");
|
||||
});
|
||||
|
||||
it("creates settings file in correct directory structure", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"test.setting": "value"}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Verify directory structure was created
|
||||
const dirResult = await execContainer(containerId, [
|
||||
"ls",
|
||||
"-la",
|
||||
"/root/.vscode-server/data/Machine/",
|
||||
]);
|
||||
|
||||
expect(dirResult.exitCode).toBe(0);
|
||||
expect(dirResult.stdout).toContain("settings.json");
|
||||
|
||||
// Verify parent directories exist
|
||||
const parentDirResult = await execContainer(containerId, [
|
||||
"ls",
|
||||
"-la",
|
||||
"/root/.vscode-server/data/",
|
||||
]);
|
||||
|
||||
expect(parentDirResult.exitCode).toBe(0);
|
||||
expect(parentDirResult.stdout).toContain("Machine");
|
||||
});
|
||||
|
||||
it("merges settings with existing settings file", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"new.setting": "new_value"}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install jq and create mock code CLI
|
||||
await execContainer(containerId, ["apt-get", "update", "-qq"]);
|
||||
await execContainer(containerId, [
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"jq",
|
||||
]);
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
// Pre-create an existing settings file
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that settings were merged (both existing and new should be present)
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
// Should contain both existing and new settings
|
||||
expect(settingsResult.stdout).toContain("existing.setting");
|
||||
expect(settingsResult.stdout).toContain("existing_value");
|
||||
expect(settingsResult.stdout).toContain("new.setting");
|
||||
expect(settingsResult.stdout).toContain("new_value");
|
||||
});
|
||||
|
||||
it("creates valid JSON settings file", async () => {
|
||||
const settings = {
|
||||
"editor.fontSize": 14,
|
||||
"editor.wordWrap": "on",
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 1000,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install jq and create mock code CLI
|
||||
await execContainer(containerId, ["apt-get", "update", "-qq"]);
|
||||
await execContainer(containerId, [
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"jq",
|
||||
]);
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Validate JSON using jq
|
||||
const jsonValidResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(jsonValidResult.exitCode).toBe(0);
|
||||
|
||||
// Extract specific values using jq
|
||||
const fontSizeResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"editor.fontSize\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(fontSizeResult.stdout.trim()).toBe("14");
|
||||
|
||||
const wordWrapResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"editor.wordWrap\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(wordWrapResult.stdout.trim()).toBe('"on"');
|
||||
|
||||
const autoSaveDelayResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"files.autoSaveDelay\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(autoSaveDelayResult.stdout.trim()).toBe("1000");
|
||||
});
|
||||
|
||||
it("installs extensions", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
extensions: '["ms-python.python", "golang.go"]',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs extension installs
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "--install-extension" ]; then
|
||||
echo "MOCK_EXTENSION_INSTALL: \$2"
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Installing extension");
|
||||
expect(result.stdout).toContain("ms-python.python");
|
||||
expect(result.stdout).toContain("golang.go");
|
||||
});
|
||||
|
||||
it("runs with correct server arguments", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
port: 9999,
|
||||
telemetry_level: "off",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that captures all arguments
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_ARGS: \$@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Check the output contains expected port message
|
||||
expect(result.stdout).toContain("Starting VS Code Web on port 9999");
|
||||
});
|
||||
|
||||
it("passes commit-id to code CLI when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
commit_id: "abc123def456",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs arguments to the log file (where output is redirected)
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_ARGS: $@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Wait briefly for background process to write to log
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Check the log file for the arguments (code CLI output goes there)
|
||||
const logResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/tmp/vscode-web.log",
|
||||
]);
|
||||
|
||||
expect(logResult.exitCode).toBe(0);
|
||||
expect(logResult.stdout).toContain("--commit-id abc123def456");
|
||||
});
|
||||
|
||||
// This test downloads and starts the real VS Code server
|
||||
it("starts real VS Code CLI and responds to healthcheck (requires network)", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
port: 13338,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install curl for downloading CLI and healthcheck
|
||||
await execContainer(containerId, ["apt-get", "update", "-qq"]);
|
||||
await execContainer(containerId, [
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"curl",
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
// Run the script - it will start the server in background
|
||||
const startResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(startResult.exitCode).toBe(0);
|
||||
expect(startResult.stdout).toContain("Starting VS Code Web");
|
||||
|
||||
// Wait for server to start and check healthcheck
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
const healthResult = await execContainer(containerId, [
|
||||
"curl",
|
||||
"-s",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"-w",
|
||||
"%{http_code}",
|
||||
"http://127.0.0.1:13338/healthz",
|
||||
]);
|
||||
|
||||
// Server should respond (200, 202, or 404 is acceptable - means server is running)
|
||||
expect(["200", "202", "404"]).toContain(healthResult.stdout.trim());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,12 +59,6 @@ variable "install_prefix" {
|
||||
default = "/tmp/vscode-web"
|
||||
}
|
||||
|
||||
variable "commit_id" {
|
||||
type = string
|
||||
description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "extensions" {
|
||||
type = list(string)
|
||||
description = "A list of extensions to install."
|
||||
@@ -105,7 +99,7 @@ variable "group" {
|
||||
|
||||
variable "settings" {
|
||||
type = any
|
||||
description = "A map of settings to apply to VS Code web."
|
||||
description = "A map of settings to apply to VS Code Web's Machine settings. These settings are merged with any existing machine settings on startup."
|
||||
default = {}
|
||||
}
|
||||
|
||||
@@ -148,25 +142,35 @@ variable "subdomain" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "platform" {
|
||||
type = string
|
||||
description = "The platform to use for the VS Code Web."
|
||||
default = ""
|
||||
validation {
|
||||
condition = var.platform == "" || var.platform == "linux" || var.platform == "darwin" || var.platform == "alpine" || var.platform == "win32"
|
||||
error_message = "Incorrect value. Please set either 'linux', 'darwin', or 'alpine' or 'win32'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "workspace" {
|
||||
type = string
|
||||
description = "Path to a .code-workspace file to open in vscode-web."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "release_channel" {
|
||||
type = string
|
||||
description = "The release channel for VS Code CLI (stable or insiders)."
|
||||
default = "stable"
|
||||
validation {
|
||||
condition = var.release_channel == "stable" || var.release_channel == "insiders"
|
||||
error_message = "Incorrect value. Please set either 'stable' or 'insiders'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "commit_id" {
|
||||
type = string
|
||||
description = "The commit SHA to use for the VS Code Server. Leave empty to use the latest version."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
locals {
|
||||
settings_b64 = var.settings != {} ? base64encode(jsonencode(var.settings)) : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "vscode-web" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "VS Code Web"
|
||||
@@ -177,8 +181,7 @@ resource "coder_script" "vscode-web" {
|
||||
INSTALL_PREFIX : var.install_prefix,
|
||||
EXTENSIONS : join(",", var.extensions),
|
||||
TELEMETRY_LEVEL : var.telemetry_level,
|
||||
// This is necessary otherwise the quotes are stripped!
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
SETTINGS_B64 : local.settings_b64,
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
DISABLE_TRUST : var.disable_trust,
|
||||
@@ -187,8 +190,8 @@ resource "coder_script" "vscode-web" {
|
||||
WORKSPACE : var.workspace,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
SERVER_BASE_PATH : local.server_base_path,
|
||||
RELEASE_CHANNEL : var.release_channel,
|
||||
COMMIT_ID : var.commit_id,
|
||||
PLATFORM : var.platform,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
|
||||
@@ -1,138 +1,417 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
CODE='\033[36;40;1m'
|
||||
EXTENSIONS=("${EXTENSIONS}")
|
||||
VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server"
|
||||
|
||||
# Set extension directory
|
||||
# Merge settings from module with existing settings file
|
||||
# Uses jq if available, falls back to Python3 for deep merge
|
||||
merge_settings() {
|
||||
local new_settings="$1"
|
||||
local settings_file="$2"
|
||||
|
||||
if [ -z "$new_settings" ] || [ "$new_settings" = "{}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$settings_file" ]; then
|
||||
mkdir -p "$(dirname "$settings_file")"
|
||||
printf '%s\n' "$new_settings" > "$settings_file"
|
||||
printf "Creating settings file...\n"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local tmpfile
|
||||
tmpfile="$(mktemp)"
|
||||
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
if jq -s '.[0] * .[1]' "$settings_file" <(printf '%s\n' "$new_settings") > "$tmpfile" 2> /dev/null; then
|
||||
mv "$tmpfile" "$settings_file"
|
||||
printf "Merging settings...\n"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v python3 > /dev/null 2>&1; then
|
||||
if python3 -c "import json,sys;m=lambda a,b:{**a,**{k:m(a[k],v)if k in a and type(a[k])==type(v)==dict else v for k,v in b.items()}};print(json.dumps(m(json.load(open(sys.argv[1])),json.loads(sys.argv[2])),indent=2))" "$settings_file" "$new_settings" > "$tmpfile" 2> /dev/null; then
|
||||
mv "$tmpfile" "$settings_file"
|
||||
printf "Merging settings...\n"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile"
|
||||
printf "Warning: Could not merge settings. Keeping existing settings.\n"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Set extension directory argument
|
||||
EXTENSION_ARG=""
|
||||
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||
fi
|
||||
|
||||
# Set extension directory
|
||||
# Set server base path argument
|
||||
SERVER_BASE_PATH_ARG=""
|
||||
if [ -n "${SERVER_BASE_PATH}" ]; then
|
||||
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
|
||||
fi
|
||||
|
||||
# Set disable workspace trust
|
||||
# Set disable workspace trust argument
|
||||
DISABLE_TRUST_ARG=""
|
||||
if [ "${DISABLE_TRUST}" = true ]; then
|
||||
DISABLE_TRUST_ARG="--disable-workspace-trust"
|
||||
fi
|
||||
|
||||
run_vscode_web() {
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
# Check if code CLI is installed
|
||||
check_code_cli() {
|
||||
if command -v code > /dev/null 2>&1; then
|
||||
echo "code"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
|
||||
echo "${INSTALL_PREFIX}/bin/code"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
|
||||
echo "⚙️ Creating settings file..."
|
||||
mkdir -p ~/.vscode-server/data/Machine
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
|
||||
fi
|
||||
|
||||
# Check if vscode-server is already installed for offline or cached mode
|
||||
if [ -f "$VSCODE_WEB" ]; then
|
||||
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
||||
echo "🥳 Found a copy of VS Code Web"
|
||||
run_vscode_web
|
||||
exit 0
|
||||
# Check if code-server is installed (fallback option)
|
||||
check_code_server() {
|
||||
if command -v code-server > /dev/null 2>&1; then
|
||||
echo "code-server"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
# Offline mode always expects a copy of vscode-server to be present
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
echo "Failed to find a copy of VS Code Web"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install prefix
|
||||
mkdir -p ${INSTALL_PREFIX}
|
||||
|
||||
printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n"
|
||||
|
||||
# Download and extract vscode-server
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported architecture"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect the platform
|
||||
if [ -n "${PLATFORM}" ]; then
|
||||
DETECTED_PLATFORM="${PLATFORM}"
|
||||
elif [ -f /etc/alpine-release ] || grep -qi 'ID=alpine' /etc/os-release 2> /dev/null || command -v apk > /dev/null 2>&1; then
|
||||
DETECTED_PLATFORM="alpine"
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
DETECTED_PLATFORM="darwin"
|
||||
else
|
||||
DETECTED_PLATFORM="linux"
|
||||
fi
|
||||
|
||||
# Check if a specific VS Code Web commit ID was provided
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
HASH="${COMMIT_ID}"
|
||||
else
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-$DETECTED_PLATFORM-$ARCH-web | cut -d '"' -f 2)
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web commit id version $HASH.\n"
|
||||
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-$DETECTED_PLATFORM-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Microsoft Visual Studio Code Server: $output"
|
||||
exit 1
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web has been installed.\n"
|
||||
|
||||
# Install each extension...
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
# shellcheck disable=SC2066
|
||||
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code-server" ]; then
|
||||
echo "${INSTALL_PREFIX}/bin/code-server"
|
||||
return 0
|
||||
fi
|
||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install extension: $extension: $output"
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
else
|
||||
# Prefer WORKSPACE if set and points to a file
|
||||
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
|
||||
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
|
||||
# Strip single-line comments then parse .extensions.recommendations[]
|
||||
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
|
||||
for extension in $extensions; do
|
||||
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
|
||||
done
|
||||
else
|
||||
# Fallback to folder-based .vscode/extensions.json (existing behavior)
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
# Find existing vscode-server binary (used by code serve-web internally)
|
||||
find_vscode_server() {
|
||||
# Check common locations for pre-downloaded vscode-server
|
||||
local server_dirs=(
|
||||
"$HOME/.vscode-server/bin"
|
||||
"$HOME/.vscode/cli/serve-web"
|
||||
)
|
||||
for dir in "$${server_dirs[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
# Find the most recent server version
|
||||
local latest
|
||||
latest=$(ls -t "$dir" 2> /dev/null | head -1)
|
||||
if [ -n "$latest" ] && [ -f "$dir/$latest/bin/code-server" ]; then
|
||||
echo "$dir/$latest/bin/code-server"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
|
||||
if [ -n "$latest" ] && [ -f "$dir/$latest/code-server" ]; then
|
||||
echo "$dir/$latest/code-server"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install VS Code CLI if not present
|
||||
install_code_cli() {
|
||||
printf "$${BOLD}Installing VS Code CLI...$${RESET}\n"
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64 | arm64) ARCH="arm64" ;;
|
||||
armv7l) ARCH="armhf" ;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect platform
|
||||
# Note: VS Code CLI uses 'alpine' for all Linux distributions
|
||||
PLATFORM=$(uname -s)
|
||||
case "$PLATFORM" in
|
||||
Linux)
|
||||
PLATFORM="alpine"
|
||||
;;
|
||||
Darwin)
|
||||
PLATFORM="darwin"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported platform: $PLATFORM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create install directory
|
||||
mkdir -p "${INSTALL_PREFIX}/bin"
|
||||
|
||||
# Download VS Code CLI
|
||||
CLI_URL="https://code.visualstudio.com/sha/download?build=${RELEASE_CHANNEL}&os=cli-$PLATFORM-$ARCH"
|
||||
printf "Downloading VS Code CLI from %s\n" "$CLI_URL"
|
||||
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -fsSL "$CLI_URL" -o "/tmp/vscode-cli.tar.gz"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -q "$CLI_URL" -O "/tmp/vscode-cli.tar.gz"
|
||||
else
|
||||
echo "Neither curl nor wget is available. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract CLI
|
||||
tar -xzf /tmp/vscode-cli.tar.gz -C "${INSTALL_PREFIX}/bin"
|
||||
rm -f /tmp/vscode-cli.tar.gz
|
||||
|
||||
# The CLI binary is named 'code'
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
|
||||
chmod +x "${INSTALL_PREFIX}/bin/code"
|
||||
export PATH="${INSTALL_PREFIX}/bin:$PATH"
|
||||
printf "$${BOLD}VS Code CLI installed successfully.$${RESET}\n"
|
||||
else
|
||||
echo "Failed to install VS Code CLI"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run VS Code Web using the code CLI (serve-web command)
|
||||
run_vscode_web_cli() {
|
||||
local CODE_CMD="$1"
|
||||
|
||||
# Build the command arguments
|
||||
ARGS="serve-web --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL}"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
|
||||
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$DISABLE_TRUST_ARG" ]; then
|
||||
ARGS="$ARGS $DISABLE_TRUST_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
ARGS="$ARGS --commit-id ${COMMIT_ID}"
|
||||
fi
|
||||
|
||||
printf "Starting VS Code Web on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$CODE_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Run VS Code Web using code-server (fallback for offline mode)
|
||||
run_code_server() {
|
||||
local SERVER_CMD="$1"
|
||||
|
||||
printf "Starting code-server on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# Build arguments for code-server
|
||||
ARGS="--port ${PORT} --host 127.0.0.1 --auth none"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$SERVER_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Run VS Code Web using vscode-server binary directly
|
||||
run_vscode_server() {
|
||||
local SERVER_CMD="$1"
|
||||
|
||||
printf "Starting VS Code Server on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# Build arguments for vscode-server
|
||||
ARGS="--port ${PORT} --host 127.0.0.1 --without-connection-token --accept-server-license-terms --telemetry-level ${TELEMETRY_LEVEL}"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
|
||||
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$SERVER_CMD" serve-local $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Install a single extension by downloading VSIX from marketplace
|
||||
install_extension_vsix() {
|
||||
local ext_id="$1"
|
||||
local publisher
|
||||
local ext_name
|
||||
publisher="$${ext_id%%.*}"
|
||||
ext_name="$${ext_id#*.}"
|
||||
|
||||
# Download VSIX from marketplace
|
||||
local vsix_url="https://$publisher.gallery.vsassets.io/_apis/public/gallery/publisher/$publisher/extension/$ext_name/latest/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage"
|
||||
local tmp_vsix="/tmp/ext-$ext_id.vsix"
|
||||
local tmp_dir="/tmp/ext-$ext_id"
|
||||
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -fsSL "$vsix_url" -o "$tmp_vsix" 2> /dev/null
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -q "$vsix_url" -O "$tmp_vsix" 2> /dev/null
|
||||
else
|
||||
echo "Failed to install extension $ext_id: neither curl nor wget available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$tmp_vsix" ]; then
|
||||
echo "Failed to download extension: $ext_id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract VSIX (it's a ZIP file)
|
||||
rm -rf "$tmp_dir"
|
||||
mkdir -p "$tmp_dir"
|
||||
if ! unzip -q "$tmp_vsix" -d "$tmp_dir" 2> /dev/null; then
|
||||
echo "Failed to extract extension: $ext_id"
|
||||
rm -f "$tmp_vsix"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get version from package.json
|
||||
local version=""
|
||||
if [ -f "$tmp_dir/extension/package.json" ]; then
|
||||
version=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$tmp_dir/extension/package.json" | head -1 | cut -d'"' -f4)
|
||||
fi
|
||||
if [ -z "$version" ]; then
|
||||
version="0.0.0"
|
||||
fi
|
||||
|
||||
# Install to extensions directory
|
||||
local ext_dir="$HOME/.vscode-server/extensions/$ext_id-$version"
|
||||
mkdir -p "$HOME/.vscode-server/extensions"
|
||||
rm -rf "$ext_dir"
|
||||
mv "$tmp_dir/extension" "$ext_dir"
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$tmp_vsix" "$tmp_dir"
|
||||
printf "Extension $ext_id v$version installed successfully.\n"
|
||||
return 0
|
||||
}
|
||||
|
||||
install_extensions() {
|
||||
# Install specified extensions
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
fi
|
||||
printf "Installing extension $extension...\n"
|
||||
install_extension_vsix "$extension"
|
||||
done
|
||||
|
||||
# Auto-install extensions from workspace or folder
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
else
|
||||
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
|
||||
printf "Installing extensions from %s...\n" "${WORKSPACE}"
|
||||
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
|
||||
for extension in $extensions; do
|
||||
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
|
||||
install_extension_vsix "$extension"
|
||||
done
|
||||
else
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
fi
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
|
||||
for extension in $extensions; do
|
||||
install_extension_vsix "$extension"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply machine settings (merge with existing if present)
|
||||
SETTINGS_B64='${SETTINGS_B64}'
|
||||
if [ -n "$SETTINGS_B64" ]; then
|
||||
SETTINGS_JSON="$(echo -n "$SETTINGS_B64" | base64 -d)"
|
||||
merge_settings "$SETTINGS_JSON" ~/.vscode-server/data/Machine/settings.json
|
||||
fi
|
||||
|
||||
run_vscode_web
|
||||
# Determine which command to use
|
||||
CODE_CMD=""
|
||||
RUN_MODE=""
|
||||
|
||||
# Check for code CLI first (preferred)
|
||||
if CODE_CMD=$(check_code_cli); then
|
||||
printf "$${BOLD}Found VS Code CLI at $CODE_CMD$${RESET}\n"
|
||||
RUN_MODE="cli"
|
||||
fi
|
||||
|
||||
# Handle offline mode
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
if [ -n "$CODE_CMD" ]; then
|
||||
# Check if vscode-server is already downloaded (code serve-web won't need to download)
|
||||
if VSCODE_SERVER=$(find_vscode_server); then
|
||||
printf "Found cached VS Code Server at $VSCODE_SERVER\n"
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
# Code CLI exists but vscode-server not cached - try using it anyway
|
||||
# (it might work if server was pre-downloaded, or fail gracefully)
|
||||
printf "Warning: VS Code Server may not be cached. Attempting to start...\n"
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try code-server as fallback for offline mode
|
||||
if SERVER_CMD=$(check_code_server); then
|
||||
printf "$${BOLD}Found code-server at $SERVER_CMD (offline fallback)$${RESET}\n"
|
||||
run_code_server "$SERVER_CMD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try vscode-server binary directly
|
||||
if VSCODE_SERVER=$(find_vscode_server); then
|
||||
printf "$${BOLD}Found VS Code Server at $VSCODE_SERVER (offline fallback)$${RESET}\n"
|
||||
run_vscode_server "$VSCODE_SERVER"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Handle use_cached mode
|
||||
if [ "${USE_CACHED}" = true ] && [ -n "$CODE_CMD" ]; then
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
install_extensions
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install VS Code CLI if not present
|
||||
if [ -z "$CODE_CMD" ]; then
|
||||
install_code_cli
|
||||
CODE_CMD="${INSTALL_PREFIX}/bin/code"
|
||||
RUN_MODE="cli"
|
||||
fi
|
||||
|
||||
# Install extensions and run VS Code Web
|
||||
install_extensions
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
run "required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
}
|
||||
}
|
||||
|
||||
run "accept_license_required" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = false
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.accept_license
|
||||
]
|
||||
}
|
||||
|
||||
run "offline_and_use_cached_conflict" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
use_cached = true
|
||||
offline = true
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "offline_disallows_extensions" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
offline = true
|
||||
extensions = ["ms-python.python", "golang.go"]
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "workspace_and_folder_conflict" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
folder = "/home/coder/project"
|
||||
workspace = "/home/coder/project.code-workspace"
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "url_with_folder_query" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
folder = "/home/coder/project"
|
||||
port = 13338
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?folder=%2Fhome%2Fcoder%2Fproject"
|
||||
error_message = "coder_app URL must include encoded folder query param"
|
||||
}
|
||||
}
|
||||
|
||||
run "url_with_workspace_query" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
workspace = "/home/coder/project.code-workspace"
|
||||
port = 13338
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?workspace=%2Fhome%2Fcoder%2Fproject.code-workspace"
|
||||
error_message = "coder_app URL must include encoded workspace query param"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_stable" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "stable"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_insiders" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "insiders"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_invalid" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "invalid"
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.release_channel
|
||||
]
|
||||
}
|
||||
|
||||
run "commit_id_empty_by_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
}
|
||||
}
|
||||
|
||||
run "commit_id_with_value" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "windsurf" {
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
|
||||
@@ -65,7 +65,7 @@ locals {
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
---
|
||||
display_name: Docker (Envbuilder)
|
||||
description: Provision envbuilder containers as Coder workspaces
|
||||
icon: ../../../../.icons/docker.svg
|
||||
verified: true
|
||||
tags: [container, docker, devcontainer, envbuilder]
|
||||
---
|
||||
|
||||
# Remote Development on Docker Containers (with Envbuilder)
|
||||
|
||||
Provision Envbuilder containers based on `devcontainer.json` as [Coder workspaces](https://coder.com/docs/workspaces) in Docker with this example template.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Infrastructure
|
||||
|
||||
Coder must have access to a running Docker socket, and the `coder` user must be a member of the `docker` group:
|
||||
|
||||
```shell
|
||||
# Add coder user to Docker group
|
||||
sudo usermod -aG docker coder
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
|
||||
# Test Docker
|
||||
sudo -u coder docker ps
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Coder supports Envbuilder containers based on `devcontainer.json` via [envbuilder](https://github.com/coder/envbuilder), an open source project. Read more about this in [Coder's documentation](https://coder.com/docs/templates/dev-containers).
|
||||
|
||||
This template provisions the following resources:
|
||||
|
||||
- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder)
|
||||
- Docker image (persistent) using [`envbuilder`](https://github.com/coder/envbuilder)
|
||||
- Docker container (ephemeral)
|
||||
- Docker volume (persistent on `/workspaces`)
|
||||
|
||||
The Git repository is cloned inside the `/workspaces` volume if not present.
|
||||
Any local changes to the Devcontainer files inside the volume will be applied when you restart the workspace.
|
||||
Keep in mind that any tools or files outside of `/workspaces` or not added as part of the Devcontainer specification are not persisted.
|
||||
Edit the `devcontainer.json` instead!
|
||||
|
||||
> **Note**
|
||||
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
|
||||
|
||||
## Docker-in-Docker
|
||||
|
||||
See the [Envbuilder documentation](https://github.com/coder/envbuilder/blob/main/docs/docker.md) for information on running Docker containers inside an Envbuilder container.
|
||||
|
||||
## Caching
|
||||
|
||||
To speed up your builds, you can use a container registry as a cache.
|
||||
When creating the template, set the parameter `cache_repo` to a valid Docker repository.
|
||||
|
||||
For example, you can run a local registry:
|
||||
|
||||
```shell
|
||||
docker run --detach \
|
||||
--volume registry-cache:/var/lib/registry \
|
||||
--publish 5000:5000 \
|
||||
--name registry-cache \
|
||||
--net=host \
|
||||
registry:2
|
||||
```
|
||||
|
||||
Then, when creating the template, enter `localhost:5000/envbuilder-cache` for the parameter `cache_repo`.
|
||||
|
||||
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
|
||||
|
||||
> [!NOTE]
|
||||
> We recommend using a registry cache with authentication enabled.
|
||||
> To allow Envbuilder to authenticate with the registry cache, specify the variable `cache_repo_docker_config_path`
|
||||
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.
|
||||
@@ -1,362 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
envbuilder = {
|
||||
source = "coder/envbuilder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
default = ""
|
||||
description = "(Optional) Docker socket URI"
|
||||
type = string
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
provider "docker" {
|
||||
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
|
||||
host = var.docker_socket != "" ? var.docker_socket : null
|
||||
}
|
||||
provider "envbuilder" {}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "repo" {
|
||||
description = "Select a repository to automatically clone and start working with a devcontainer."
|
||||
display_name = "Repository (auto)"
|
||||
mutable = true
|
||||
name = "repo"
|
||||
option {
|
||||
name = "vercel/next.js"
|
||||
description = "The React Framework"
|
||||
value = "https://github.com/vercel/next.js"
|
||||
}
|
||||
option {
|
||||
name = "home-assistant/core"
|
||||
description = "🏡 Open source home automation that puts local control and privacy first."
|
||||
value = "https://github.com/home-assistant/core"
|
||||
}
|
||||
option {
|
||||
name = "discourse/discourse"
|
||||
description = "A platform for community discussion. Free, open, simple."
|
||||
value = "https://github.com/discourse/discourse"
|
||||
}
|
||||
option {
|
||||
name = "denoland/deno"
|
||||
description = "A modern runtime for JavaScript and TypeScript."
|
||||
value = "https://github.com/denoland/deno"
|
||||
}
|
||||
option {
|
||||
name = "microsoft/vscode"
|
||||
icon = "/icon/code.svg"
|
||||
description = "Code editing. Redefined."
|
||||
value = "https://github.com/microsoft/vscode"
|
||||
}
|
||||
option {
|
||||
name = "Custom"
|
||||
icon = "/emojis/1f5c3.png"
|
||||
description = "Specify a custom repo URL below"
|
||||
value = "custom"
|
||||
}
|
||||
order = 1
|
||||
}
|
||||
|
||||
data "coder_parameter" "custom_repo_url" {
|
||||
default = ""
|
||||
description = "Optionally enter a custom repository URL, see [awesome-devcontainers](https://github.com/manekinekko/awesome-devcontainers)."
|
||||
display_name = "Repository URL (custom)"
|
||||
name = "custom_repo_url"
|
||||
mutable = true
|
||||
order = 2
|
||||
}
|
||||
|
||||
data "coder_parameter" "fallback_image" {
|
||||
default = "codercom/enterprise-base:ubuntu"
|
||||
description = "This image runs if the devcontainer fails to build."
|
||||
display_name = "Fallback Image"
|
||||
mutable = true
|
||||
name = "fallback_image"
|
||||
order = 3
|
||||
}
|
||||
|
||||
data "coder_parameter" "devcontainer_builder" {
|
||||
description = <<-EOF
|
||||
Image that will build the devcontainer.
|
||||
We highly recommend using a specific release as the `:latest` tag will change.
|
||||
Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder
|
||||
EOF
|
||||
display_name = "Devcontainer Builder"
|
||||
mutable = true
|
||||
name = "devcontainer_builder"
|
||||
default = "ghcr.io/coder/envbuilder:latest"
|
||||
order = 4
|
||||
}
|
||||
|
||||
variable "cache_repo" {
|
||||
default = ""
|
||||
description = "(Optional) Use a container registry as a cache to speed up builds."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "insecure_cache_repo" {
|
||||
default = false
|
||||
description = "Enable this option if your cache registry does not serve HTTPS."
|
||||
type = bool
|
||||
}
|
||||
|
||||
variable "cache_repo_docker_config_path" {
|
||||
default = ""
|
||||
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required."
|
||||
sensitive = true
|
||||
type = string
|
||||
}
|
||||
|
||||
locals {
|
||||
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
|
||||
git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
git_author_email = data.coder_workspace_owner.me.email
|
||||
repo_url = data.coder_parameter.repo.value == "custom" ? data.coder_parameter.custom_repo_url.value : data.coder_parameter.repo.value
|
||||
# The envbuilder provider requires a key-value map of environment variables.
|
||||
envbuilder_env = {
|
||||
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
|
||||
# if the cache repo is enabled.
|
||||
"ENVBUILDER_GIT_URL" : local.repo_url,
|
||||
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
|
||||
"CODER_AGENT_TOKEN" : coder_agent.main.token,
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
"CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
|
||||
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
|
||||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
|
||||
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
|
||||
"ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}",
|
||||
}
|
||||
# Convert the above map to the format expected by the docker provider.
|
||||
docker_env = [
|
||||
for k, v in local.envbuilder_env : "${k}=${v}"
|
||||
]
|
||||
}
|
||||
|
||||
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
|
||||
count = var.cache_repo_docker_config_path == "" ? 0 : 1
|
||||
filename = var.cache_repo_docker_config_path
|
||||
}
|
||||
|
||||
resource "docker_image" "devcontainer_builder_image" {
|
||||
name = local.devcontainer_builder_image
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
resource "docker_volume" "workspaces" {
|
||||
name = "coder-${data.coder_workspace.me.id}"
|
||||
# Protect the volume from being deleted due to changes in attributes.
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# This field becomes outdated if the workspace is renamed but can
|
||||
# be useful for debugging or cleaning out dangling volumes.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
# Check for the presence of a prebuilt image in the cache repo
|
||||
# that we can use instead.
|
||||
resource "envbuilder_cached_image" "cached" {
|
||||
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
|
||||
builder_image = local.devcontainer_builder_image
|
||||
git_url = local.repo_url
|
||||
cache_repo = var.cache_repo
|
||||
extra_env = local.envbuilder_env
|
||||
insecure = var.insecure_cache_repo
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
# Use the environment specified by the envbuilder provider, if available.
|
||||
env = var.cache_repo == "" ? local.docker_env : envbuilder_cached_image.cached.0.env
|
||||
# network_mode = "host" # Uncomment if testing with a registry running on `localhost`.
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/workspaces"
|
||||
volume_name = docker_volume.workspaces.name
|
||||
read_only = false
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
|
||||
EOT
|
||||
dir = "/workspaces"
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = local.git_author_name
|
||||
GIT_AUTHOR_EMAIL = local.git_author_email
|
||||
GIT_COMMITTER_NAME = local.git_author_name
|
||||
GIT_COMMITTER_EMAIL = local.git_author_email
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $HOME"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/code-server
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/jetbrains
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/workspaces"
|
||||
}
|
||||
|
||||
resource "coder_metadata" "container_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = coder_agent.main.id
|
||||
item {
|
||||
key = "workspace image"
|
||||
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
}
|
||||
item {
|
||||
key = "git url"
|
||||
value = local.repo_url
|
||||
}
|
||||
item {
|
||||
key = "cache repo"
|
||||
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
display_name: Tasks on Docker
|
||||
description: Run Coder Tasks on Docker with an example application
|
||||
icon: ../../../../.icons/tasks.svg
|
||||
verified: false
|
||||
tags: [docker, container, ai, tasks]
|
||||
---
|
||||
|
||||
# Run Coder Tasks on Docker
|
||||
|
||||
This is an example template for running [Coder Tasks](https://coder.com/docs/ai-coder/tasks), Claude Code, along with a [real world application](https://realworld-docs.netlify.app/).
|
||||
|
||||

|
||||
|
||||
This is a fantastic starting point for working with AI agents with Coder Tasks. Try prompts such as:
|
||||
|
||||
- "Make the background color blue"
|
||||
- "Add a dark mode"
|
||||
- "Rewrite the entire backend in Go"
|
||||
|
||||
## Included in this template
|
||||
|
||||
This template is designed to be an example and a reference for building other templates with Coder Tasks. You can always run Coder Tasks on different infrastructure (e.g. as on Kubernetes, VMs) and with your own GitHub repositories, MCP servers, images, etc.
|
||||
|
||||
Additionally, this template uses our [Claude Code](https://registry.coder.com/modules/coder/claude-code) module, but [other agents](https://registry.coder.com/modules?search=tag%3Aagent) or even [custom agents](https://coder.com/docs/ai-coder/custom-agents) can be used in its place.
|
||||
|
||||
This template uses a [Workspace Preset](https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets) that pre-defines:
|
||||
|
||||
- Universal Container Image (e.g. contains Node.js, Java, Python, Ruby, etc)
|
||||
- MCP servers (desktop-commander for long-running logs, playwright for previewing changes)
|
||||
- System prompt and [repository](https://github.com/coder-contrib/realworld-django-rest-framework-angular) for the AI agent
|
||||
- Startup script to initialize the repository and start the development server
|
||||
|
||||
## Add this template to your Coder deployment
|
||||
|
||||
You can also add this template to your Coder deployment and begin tinkering right away!
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Coder installed (see [our docs](https://coder.com/docs/install)), ideally a Linux VM with Docker
|
||||
- Anthropic API Key (or access to Anthropic models via Bedrock or Vertex, see [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code/third-party-integrations))
|
||||
- Access to a Docker socket
|
||||
- If on the local VM, ensure the `coder` user is added to the Docker group (docs)
|
||||
|
||||
```sh
|
||||
# Add coder user to Docker group
|
||||
sudo adduser coder docker
|
||||
|
||||
# Restart Coder server
|
||||
sudo systemctl restart coder
|
||||
|
||||
# Test Docker
|
||||
sudo -u coder docker ps
|
||||
```
|
||||
|
||||
- If on a remote VM, see the [Docker Terraform provider documentation](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs#remote-hosts) to configure a remote host
|
||||
|
||||
To import this template into Coder, first create a template from "Scratch" in the template editor.
|
||||
|
||||
Visit this URL for your Coder deployment:
|
||||
|
||||
```sh
|
||||
https://coder.example.com/templates/new?exampleId=scratch
|
||||
```
|
||||
|
||||
After creating the template, paste the contents from [main.tf](https://github.com/coder/registry/blob/main/registry/coder/templates/tasks-docker/main.tf) into the template editor and save.
|
||||
|
||||
Alternatively, you can use the Coder CLI to [push the template](https://coder.com/docs/reference/cli/templates_push)
|
||||
|
||||
```sh
|
||||
# Download the CLI
|
||||
curl -L https://coder.com/install.sh | sh
|
||||
|
||||
# Log in to your deployment
|
||||
coder login https://coder.example.com
|
||||
|
||||
# Clone the registry
|
||||
git clone https://github.com/coder/registry
|
||||
cd registry
|
||||
|
||||
# Navigate to this template
|
||||
cd registry/coder/templates/tasks-docker
|
||||
|
||||
# Push the template
|
||||
coder templates push
|
||||
```
|
||||
@@ -1,380 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.13"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# This template requires a valid Docker socket
|
||||
# However, you can reference our Kubernetes/VM
|
||||
# example templates and adapt the Claude Code module
|
||||
#
|
||||
# See: https://registry.coder.com/templates
|
||||
provider "docker" {}
|
||||
|
||||
# A `coder_ai_task` resource enables Tasks and associates
|
||||
# the task with the coder_app that will act as an AI agent.
|
||||
resource "coder_ai_task" "task" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
app_id = module.claude-code[count.index].task_app_id
|
||||
}
|
||||
|
||||
# You can read the task prompt from the `coder_task` data source.
|
||||
data "coder_task" "me" {}
|
||||
|
||||
# The Claude Code module does the automatic task reporting
|
||||
# Other agent modules: https://registry.coder.com/modules?search=agent
|
||||
# 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.7.5"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/projects"
|
||||
order = 999
|
||||
claude_api_key = ""
|
||||
ai_prompt = data.coder_task.me.prompt
|
||||
system_prompt = data.coder_parameter.system_prompt.value
|
||||
model = "sonnet"
|
||||
permission_mode = "plan"
|
||||
post_install_script = data.coder_parameter.setup_script.value
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
# See https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets
|
||||
data "coder_workspace_preset" "default" {
|
||||
name = "Real World App: Angular + Django"
|
||||
default = true
|
||||
parameters = {
|
||||
"system_prompt" = <<-EOT
|
||||
-- Framing --
|
||||
You are a helpful assistant that can help with code. You are running inside a Coder Workspace and provide status updates to the user via Coder MCP. Stay on track, feel free to debug, but when the original plan fails, do not choose a different route/architecture without checking the user first.
|
||||
|
||||
-- Tool Selection --
|
||||
- playwright: previewing your changes after you made them
|
||||
to confirm it worked as expected
|
||||
- desktop-commander - use only for commands that keep running
|
||||
(servers, dev watchers, GUI apps).
|
||||
- Built-in tools - use for everything else:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
Remember this decision rule:
|
||||
- Stays running? → desktop-commander
|
||||
- Finishes immediately? → built-in tools
|
||||
|
||||
-- Context --
|
||||
There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it.
|
||||
|
||||
Since this app is for demo purposes and the user is previewing the homepage and subsequent pages, aim to make the first visual change/prototype very quickly so the user can preview it, then focus on backend or logic which can be a more involved, long-running architecture plan.
|
||||
|
||||
EOT
|
||||
|
||||
"setup_script" = <<-EOT
|
||||
# Set up projects dir
|
||||
mkdir -p /home/coder/projects
|
||||
cd $HOME/projects
|
||||
|
||||
# Packages: Install additional packages
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
if ! command -v google-chrome >/dev/null 2>&1; then
|
||||
yes | npx playwright install chrome
|
||||
fi
|
||||
|
||||
# MCP: Install and configure MCP Servers
|
||||
npm install -g @wonderwhy-er/desktop-commander
|
||||
claude mcp add playwright npx -- @playwright/mcp@latest --headless --isolated --no-sandbox
|
||||
claude mcp add desktop-commander desktop-commander
|
||||
|
||||
# Repo: Clone and pull changes from the git repository
|
||||
if [ ! -d "realworld-django-rest-framework-angular" ]; then
|
||||
git clone https://github.com/coder-contrib/realworld-django-rest-framework-angular.git
|
||||
else
|
||||
cd realworld-django-rest-framework-angular
|
||||
git fetch
|
||||
# Check for uncommitted changes
|
||||
if git diff-index --quiet HEAD -- && \
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] && \
|
||||
[ -z "$(git log --branches --not --remotes)" ]; then
|
||||
echo "Repo is clean. Pulling latest changes..."
|
||||
git pull
|
||||
else
|
||||
echo "Repo has uncommitted or unpushed changes. Skipping pull."
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# Initialize: Start the development server
|
||||
cd realworld-django-rest-framework-angular && ./start-dev.sh
|
||||
EOT
|
||||
"preview_port" = "4200"
|
||||
"container_image" = "codercom/example-universal:ubuntu"
|
||||
}
|
||||
|
||||
# Pre-builds is a Coder Premium
|
||||
# feature to speed up workspace creation
|
||||
#
|
||||
# see https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces
|
||||
# prebuilds {
|
||||
# instances = 1
|
||||
# expiration_policy {
|
||||
# ttl = 86400 # Time (in seconds) after which unclaimed prebuilds are expired (1 day)
|
||||
# }
|
||||
# }
|
||||
}
|
||||
|
||||
# Advanced parameters (these are all set via preset)
|
||||
data "coder_parameter" "system_prompt" {
|
||||
name = "system_prompt"
|
||||
display_name = "System Prompt"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "System prompt for the agent with generalized instructions"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "setup_script" {
|
||||
name = "setup_script"
|
||||
display_name = "Setup Script"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "Script to run before running the agent"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "container_image" {
|
||||
name = "container_image"
|
||||
display_name = "Container Image"
|
||||
type = "string"
|
||||
default = "codercom/example-universal:ubuntu"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "preview_port" {
|
||||
name = "preview_port"
|
||||
display_name = "Preview Port"
|
||||
description = "The port the web app is running to preview in Tasks"
|
||||
type = "number"
|
||||
default = "3000"
|
||||
mutable = false
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
# Prepare user home with default files on first start.
|
||||
if [ ! -f ~/.init_done ]; then
|
||||
cp -rT /etc/skel ~
|
||||
touch ~/.init_done
|
||||
fi
|
||||
EOT
|
||||
|
||||
# These environment variables allow you to make Git commits right away after creating a
|
||||
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
|
||||
# You can remove this block if you'd prefer to configure Git manually or using
|
||||
# dotfiles. (see docs/dotfiles.md)
|
||||
env = {
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Load Average (Host)"
|
||||
key = "6_load_host"
|
||||
# get load avg scaled by number of cores
|
||||
script = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/code-server
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
folder = "/home/coder/projects"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
settings = {
|
||||
"workbench.colorTheme" : "Default Dark Modern"
|
||||
}
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/home/coder/projects"
|
||||
}
|
||||
|
||||
resource "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
# Protect the volume from being deleted due to changes in attributes.
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# This field becomes outdated if the workspace is renamed but can
|
||||
# be useful for debugging or cleaning out dangling volumes.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "preview" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "preview"
|
||||
display_name = "Preview your app"
|
||||
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}"
|
||||
share = "authenticated"
|
||||
subdomain = true
|
||||
open_in = "tab"
|
||||
order = 0
|
||||
healthcheck {
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}/"
|
||||
interval = 5
|
||||
threshold = 15
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = data.coder_parameter.container_image.value
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Hostname makes the shell more user friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
user = "coder"
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home_volume.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
# Add labels in Docker to keep track of orphan resources.
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "positron" {
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -41,13 +41,13 @@ variable "group" {
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "cursor"
|
||||
default = "positron"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Cursor Desktop"
|
||||
default = "Positron Desktop"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
@@ -55,7 +55,7 @@ data "coder_workspace_owner" "me" {}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
|
||||
Reference in New Issue
Block a user