From eae721f792b16bb9ac832e045802595636409f30 Mon Sep 17 00:00:00 2001 From: Coder Module Mirror Date: Fri, 13 Mar 2026 15:55:25 +0100 Subject: [PATCH] mirror: registry.coder.com/coder/code-server/coder v1.4.3 --- README.md | 130 +++++++++++++++++++++++++ code-server.tftest.hcl | 50 ++++++++++ main.test.ts | 84 ++++++++++++++++ main.tf | 211 +++++++++++++++++++++++++++++++++++++++++ run.sh | 143 ++++++++++++++++++++++++++++ 5 files changed, 618 insertions(+) create mode 100644 README.md create mode 100644 code-server.tftest.hcl create mode 100644 main.test.ts create mode 100644 main.tf create mode 100644 run.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..fdb3f1a --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +--- +display_name: code-server +description: VS Code in the browser +icon: ../../../../.icons/code.svg +verified: true +tags: [ide, web, code-server] +--- + +# code-server + +Automatically install [code-server](https://github.com/coder/code-server) in a workspace, create an app to access it via the dashboard, install extensions, and pre-configure editor settings. + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id +} +``` + +![Screenshot 1](https://github.com/coder/code-server/raw/main/docs/assets/screenshot-1.png?raw=true) + +## Examples + +### Pin Versions + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + install_version = "4.106.3" +} +``` + +### Pre-install Extensions + +Install the Dracula theme from [OpenVSX](https://open-vsx.org/): + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + extensions = [ + "dracula-theme.theme-dracula" + ] +} +``` + +Enter the `.` into the extensions array and code-server will automatically install on start. + +### Pre-configure Settings + +Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + extensions = ["dracula-theme.theme-dracula"] + settings = { + "workbench.colorTheme" = "Dracula" + } +} +``` + +### Install multiple extensions + +Just run code-server in the background, don't fetch it from GitHub: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] +} +``` + +### Pass Additional Arguments + +You can pass additional command-line arguments to code-server using the `additional_args` variable. For example, to disable workspace trust: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + additional_args = "--disable-workspace-trust" +} +``` + +### Offline and Use Cached Modes + +By default the module looks for code-server at `/tmp/code-server` but this can be changed with `install_prefix`. + +Run an existing copy of code-server if found, otherwise download from GitHub: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + use_cached = true + extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] +} +``` + +Just run code-server in the background, don't fetch it from GitHub: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.3" + agent_id = coder_agent.example.id + offline = true +} +``` + +Some of the key differences between code-server and [VS Code Web](https://registry.coder.com/modules/coder/vscode-web) are listed in [docs](https://coder.com/docs/user-guides/workspace-access/code-server#differences-between-code-server-and-vs-code-web). diff --git a/code-server.tftest.hcl b/code-server.tftest.hcl new file mode 100644 index 0000000..ebbb717 --- /dev/null +++ b/code-server.tftest.hcl @@ -0,0 +1,50 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "offline_and_use_cached_conflict" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + offline = true + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "offline_disallows_extensions" { + command = plan + + variables { + agent_id = "foo" + offline = true + extensions = ["ms-python.python", "golang.go"] + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "url_with_folder_query" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder/project" + port = 13337 + } + + assert { + condition = resource.coder_app.code-server.url == "http://localhost:13337/?folder=%2Fhome%2Fcoder%2Fproject" + error_message = "coder_app URL must include encoded folder query param" + } +} diff --git a/main.test.ts b/main.test.ts new file mode 100644 index 0000000..914ae2f --- /dev/null +++ b/main.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "bun:test"; +import { + execContainer, + findResourceInstance, + removeContainer, + runContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("code-server", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("use_cached and offline can not be used together", () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + use_cached: "true", + offline: "true", + }); + }; + expect(t).toThrow("Offline and Use Cached can not be used together"); + }); + + it("offline and extensions can not be used together", () => { + const t = async () => { + await runTerraformApply(import.meta.dir, { + agent_id: "foo", + offline: "true", + extensions: '["1", "2"]', + }); + }; + expect(t).toThrow("Offline mode does not allow extensions to be installed"); + }); + + it("installs and runs code-server", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const id = await runContainer("ubuntu:latest"); + try { + await execContainer(id, [ + "bash", + "-c", + "apt-get update && apt-get install -y curl", + ]); + + const script = findResourceInstance(state, "coder_script").script; + const result = await execContainer(id, ["bash", "-c", script]); + if (result.exitCode !== 0) { + console.log(result.stdout); + console.log(result.stderr); + } + expect(result.exitCode).toBe(0); + + const version = await execContainer(id, [ + "/tmp/code-server/bin/code-server", + "--version", + ]); + expect(version.exitCode).toBe(0); + expect(version.stdout).toMatch(/\d+\.\d+\.\d+/); + + const health = await execContainer(id, [ + "curl", + "--retry", + "10", + "--retry-delay", + "1", + "--retry-all-errors", + "-sf", + "http://localhost:13337/healthz", + ]); + expect(health.exitCode).toBe(0); + } finally { + await removeContainer(id); + } + }, 60000); +}); diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..090b6a5 --- /dev/null +++ b/main.tf @@ -0,0 +1,211 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "extensions" { + type = list(string) + description = "A list of extensions to install." + default = [] +} + +variable "port" { + type = number + description = "The port to run code-server on." + default = 13337 +} + +variable "display_name" { + type = string + description = "The display name for the code-server application." + default = "code-server" +} + +variable "slug" { + type = string + description = "The slug for the code-server application." + default = "code-server" +} + +variable "settings" { + type = any + description = "A map of settings to apply to code-server." + default = {} +} + +variable "machine_settings" { + type = any + description = "A map of template level machine settings to apply to code-server. This will be overwritten at each container start." + default = {} +} + +variable "folder" { + type = string + description = "The folder to open in code-server." + default = "" +} + +variable "install_prefix" { + type = string + description = "The prefix to install code-server to." + default = "/tmp/code-server" +} + +variable "log_path" { + type = string + description = "The path to log code-server to." + default = "/tmp/code-server.log" +} + +variable "install_version" { + type = string + description = "The version of code-server to install." + default = "" +} + +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "offline" { + type = bool + description = "Just run code-server in the background, don't fetch it from GitHub" + default = false +} + +variable "use_cached" { + type = bool + description = "Uses cached copy code-server in the background, otherwise fetched it from GitHub" + default = false +} + +variable "use_cached_extensions" { + type = bool + description = "Uses cached copy of extensions, otherwise do a forced upgrade" + default = false +} + +variable "extensions_dir" { + type = string + description = "Override the directory to store extensions in." + default = "" +} + +variable "auto_install_extensions" { + type = bool + description = "Automatically install recommended extensions when code-server starts." + default = false +} + +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = false +} + +variable "open_in" { + type = string + description = <<-EOT + Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`. + `"tab"` opens in a new tab in the same browser window. + `"slim-window"` opens a new browser window without navigation controls. + EOT + default = "slim-window" + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'." + } +} + +variable "additional_args" { + type = string + description = "Additional command-line arguments to pass to code-server (e.g., '--disable-workspace-trust')." + default = "" +} + +resource "coder_script" "code-server" { + agent_id = var.agent_id + display_name = "code-server" + icon = "/icon/code.svg" + script = templatefile("${path.module}/run.sh", { + VERSION : var.install_version, + EXTENSIONS : join(",", var.extensions), + APP_NAME : var.display_name, + PORT : var.port, + LOG_PATH : var.log_path, + INSTALL_PREFIX : var.install_prefix, + // This is necessary otherwise the quotes are stripped! + SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), + MACHINE_SETTINGS : replace(jsonencode(var.machine_settings), "\"", "\\\""), + OFFLINE : var.offline, + USE_CACHED : var.use_cached, + USE_CACHED_EXTENSIONS : var.use_cached_extensions, + EXTENSIONS_DIR : var.extensions_dir, + FOLDER : var.folder, + AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, + ADDITIONAL_ARGS : var.additional_args, + }) + run_on_start = true + + lifecycle { + precondition { + condition = !var.offline || length(var.extensions) == 0 + error_message = "Offline mode does not allow extensions to be installed" + } + + precondition { + condition = !var.offline || !var.use_cached + error_message = "Offline and Use Cached can not be used together" + } + } +} + +resource "coder_app" "code-server" { + agent_id = var.agent_id + slug = var.slug + display_name = var.display_name + url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}" + icon = "/icon/code.svg" + subdomain = var.subdomain + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + healthcheck { + url = "http://localhost:${var.port}/healthz" + interval = 5 + threshold = 6 + } +} diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..33a6972 --- /dev/null +++ b/run.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +EXTENSIONS=("${EXTENSIONS}") +BOLD='\033[0;1m' +CODE='\033[36;40;1m' +RESET='\033[0m' +CODE_SERVER="${INSTALL_PREFIX}/bin/code-server" + +# Set extension directory +EXTENSION_ARG="" +if [ -n "${EXTENSIONS_DIR}" ]; then + EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}" + mkdir -p "${EXTENSIONS_DIR}" +fi + +function run_code_server() { + echo "👷 Running code-server in the background..." + echo "Check logs at ${LOG_PATH}!" + $CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" ${ADDITIONAL_ARGS} > "${LOG_PATH}" 2>&1 & +} + +# Check if the settings file exists... +if [ ! -f ~/.local/share/code-server/User/settings.json ]; then + echo "⚙️ Creating settings file..." + mkdir -p ~/.local/share/code-server/User + if command -v jq &> /dev/null; then + echo "${SETTINGS}" | jq '.' > ~/.local/share/code-server/User/settings.json + else + echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json + fi +fi + +# Apply/overwrite template based settings +echo "⚙️ Creating machine settings file..." +mkdir -p ~/.local/share/code-server/Machine +if command -v jq &> /dev/null; then + echo "${MACHINE_SETTINGS}" | jq '.' > ~/.local/share/code-server/Machine/settings.json +else + echo "${MACHINE_SETTINGS}" > ~/.local/share/code-server/Machine/settings.json +fi + +# Check if code-server is already installed for offline +if [ "${OFFLINE}" = true ]; then + if [ -f "$CODE_SERVER" ]; then + echo "🥳 Found a copy of code-server" + run_code_server + exit 0 + fi + # Offline mode always expects a copy of code-server to be present + echo "Failed to find a copy of code-server" + exit 1 +fi + +# If there is no cached install OR we don't want to use a cached install +if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then + printf "$${BOLD}Installing code-server!\n" + + # Clean up from other install (in case install prefix changed). + if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/code-server" ]; then + rm "$CODER_SCRIPT_BIN_DIR/code-server" + fi + + ARGS=( + "--method=standalone" + "--prefix=${INSTALL_PREFIX}" + ) + if [ -n "${VERSION}" ]; then + ARGS+=("--version=${VERSION}") + fi + + output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}") + if [ $? -ne 0 ]; then + echo "Failed to install code-server: $output" + exit 1 + fi + printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n" +fi + +# Make the code-server available in PATH. +if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/code-server" ]; then + ln -s "$CODE_SERVER" "$CODER_SCRIPT_BIN_DIR/code-server" +fi + +# Get the list of installed extensions... +LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG) +readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS" +function extension_installed() { + if [ "${USE_CACHED_EXTENSIONS}" != true ]; then + return 1 + fi + # shellcheck disable=SC2066 + for _extension in "$${EXTENSIONS_ARRAY[@]}"; do + if [ "$_extension" == "$1" ]; then + echo "Extension $1 was already installed." + return 0 + fi + done + return 1 +} + +# Install each extension... +IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" +# shellcheck disable=SC2066 +for extension in "$${EXTENSIONLIST[@]}"; do + if [ -z "$extension" ]; then + continue + fi + if extension_installed "$extension"; then + continue + fi + printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" + output=$($CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension") + if [ $? -ne 0 ]; then + echo "Failed to install extension: $extension: $output" + exit 1 + fi +done + +if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then + if ! command -v jq > /dev/null; then + echo "jq is required to install extensions from a workspace file." + exit 0 + fi + + 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" + # Use sed to remove single-line comments before parsing with jq + extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]') + for extension in $extensions; do + if extension_installed "$extension"; then + continue + fi + $CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension" + done + fi +fi + +run_code_server