diff --git a/.sample/main.tf b/.sample/main.tf index 733f121..9cbb464 100644 --- a/.sample/main.tf +++ b/.sample/main.tf @@ -69,7 +69,7 @@ resource "coder_app" "MODULE_NAME" { slug = "MODULE_NAME" display_name = "MODULE_NAME" url = "http://localhost:${var.port}" - icon = loocal.icon_url + icon = local.icon_url subdomain = false share = "owner" diff --git a/bun.lockb b/bun.lockb index d7e77b2..d3e2214 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/hcp-vault-secrets/README.md b/hcp-vault-secrets/README.md index 377ad5a..fc71230 100644 --- a/hcp-vault-secrets/README.md +++ b/hcp-vault-secrets/README.md @@ -14,10 +14,11 @@ This module lets you fetch all or selective secrets from a [HCP Vault Secrets](h ```tf module "vault" { - source = "registry.coder.com/modules/hcp-vault-secrets/coder" - version = "1.0.3" - agent_id = coder_agent.example.id - app_name = "demo-app" + source = "registry.coder.com/modules/hcp-vault-secrets/coder" + version = "1.0.7" + agent_id = coder_agent.example.id + app_name = "demo-app" + project_id = "aaa-bbb-ccc" } ``` @@ -29,6 +30,7 @@ To configure the HCP Vault Secrets module, follow these steps, 2. Create an HCP Service Principal from the HCP Vault Secrets app in the HCP console. This will give you the `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` that you need to authenticate with HCP Vault Secrets. ![HCP vault secrets credentials](../.images/hcp-vault-secrets-credentials.png) 3. Set `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` variables on the coder provisioner (recommended) or supply them as input to the module. +4. Set the `project_id`. This is the ID of the project where the HCP Vault Secrets app is running. > See the [HCP Vault Secrets documentation](https://developer.hashicorp.com/hcp/docs/vault-secrets) for more information. @@ -38,10 +40,11 @@ To fetch all secrets from the HCP Vault Secrets app, skip the `secrets` input. ```tf module "vault" { - source = "registry.coder.com/modules/hcp-vault-secrets/coder" - version = "1.0.3" - agent_id = coder_agent.example.id - app_name = "demo-app" + source = "registry.coder.com/modules/hcp-vault-secrets/coder" + version = "1.0.7" + agent_id = coder_agent.example.id + app_name = "demo-app" + project_id = "aaa-bbb-ccc" } ``` @@ -51,11 +54,12 @@ To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` inp ```tf module "vault" { - source = "registry.coder.com/modules/hcp-vault-secrets/coder" - version = "1.0.3" - agent_id = coder_agent.example.id - app_name = "demo-app" - secrets = ["MY_SECRET_1", "MY_SECRET_2"] + source = "registry.coder.com/modules/hcp-vault-secrets/coder" + version = "1.0.7" + agent_id = coder_agent.example.id + app_name = "demo-app" + project_id = "aaa-bbb-ccc" + secrets = ["MY_SECRET_1", "MY_SECRET_2"] } ``` @@ -66,9 +70,10 @@ Set `client_id` and `client_secret` as module inputs. ```tf module "vault" { source = "registry.coder.com/modules/hcp-vault-secrets/coder" - version = "1.0.3" + version = "1.0.7" agent_id = coder_agent.example.id app_name = "demo-app" + project_id = "aaa-bbb-ccc" client_id = "HCP_CLIENT_ID" client_secret = "HCP_CLIENT_SECRET" } diff --git a/hcp-vault-secrets/main.tf b/hcp-vault-secrets/main.tf index 40ab283..9a5e94b 100644 --- a/hcp-vault-secrets/main.tf +++ b/hcp-vault-secrets/main.tf @@ -16,6 +16,7 @@ terraform { provider "hcp" { client_id = var.client_id client_secret = var.client_secret + project_id = var.project_id } provider "coder" {} @@ -25,6 +26,11 @@ variable "agent_id" { description = "The ID of a Coder agent." } +variable "project_id" { + type = string + description = "The ID of the HCP project." +} + variable "client_id" { type = string description = <<-EOF diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 629f850..a9c6ac3 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -14,7 +14,7 @@ This module adds a JetBrains Gateway Button to open any workspace with a single ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.3" + version = "1.0.6" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -32,7 +32,7 @@ module "jetbrains_gateway" { ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.3" + version = "1.0.6" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" diff --git a/jetbrains-gateway/main.test.ts b/jetbrains-gateway/main.test.ts index bc0ef2f..b327e41 100644 --- a/jetbrains-gateway/main.test.ts +++ b/jetbrains-gateway/main.test.ts @@ -11,18 +11,16 @@ describe("jetbrains-gateway", async () => { await testRequiredVariables(import.meta.dir, { agent_id: "foo", agent_name: "foo", - folder: "/baz/", + folder: "/home/foo", }); it("default to first ide", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", agent_name: "foo", - folder: "/baz/", + folder: "/home/foo", jetbrains_ides: '["IU", "GO", "PY"]', }); - expect(state.outputs.jetbrains_ides.value).toBe( - '["IU","232.10203.10","https://download.jetbrains.com/idea/ideaIU-2023.2.4.tar.gz"]', - ); + expect(state.outputs.identifier.value).toBe("IU"); }); }); diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index af2a0ea..b7bd5c3 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -22,6 +22,10 @@ variable "agent_name" { variable "folder" { type = string description = "The directory to open in the IDE. e.g. /home/coder/project" + validation { + condition = can(regex("^(?:/[^/]+)+$", var.folder)) + error_message = "The folder must be a full path and must not start with a ~." + } } variable "default" { @@ -30,10 +34,6 @@ variable "default" { description = "Default IDE" } -locals { - supported_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM"] -} - variable "jetbrains_ide_versions" { type = map(object({ build_number = string @@ -69,29 +69,28 @@ variable "jetbrains_ide_versions" { build_number = "232.10203.15" version = "2023.2.4" } - } validation { condition = ( alltrue([ - for code in var.jetbrains_ide_versions : contains(local.supported_ides, code) + for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM"], code) ]) ) - error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", local.supported_ides)}." + error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM"])}." } } variable "jetbrains_ides" { type = list(string) description = "The list of IDE product codes." - default = local.supported_ides + default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM"] validation { condition = ( alltrue([ - for code in var.jetbrains_ides : contains(local.supported_ides, code) + for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM"], code) ]) ) - error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", local.supported_ides)}." + error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM"])}." } # check if the list is empty validation { @@ -108,58 +107,71 @@ variable "jetbrains_ides" { locals { jetbrains_ides = { "GO" = { - icon = "/icon/goland.svg", - name = "GoLand", - value = jsonencode(["GO", var.jetbrains_ide_versions["GO"].build_number, "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"]) + icon = "/icon/goland.svg", + name = "GoLand", + identifier = "GO", + build_number = var.jetbrains_ide_versions["GO"].build_number, + download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz" }, "WS" = { - icon = "/icon/webstorm.svg", - name = "WebStorm", - value = jsonencode(["WS", var.jetbrains_ide_versions["WS"].build_number, "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"]) + icon = "/icon/webstorm.svg", + name = "WebStorm", + identifier = "WS", + build_number = var.jetbrains_ide_versions["WS"].build_number, + download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz" }, "IU" = { - icon = "/icon/intellij.svg", - name = "IntelliJ IDEA Ultimate", - value = jsonencode(["IU", var.jetbrains_ide_versions["IU"].build_number, "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"]) + icon = "/icon/intellij.svg", + name = "IntelliJ IDEA Ultimate", + identifier = "IU", + build_number = var.jetbrains_ide_versions["IU"].build_number, + download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz" }, "PY" = { - icon = "/icon/pycharm.svg", - name = "PyCharm Professional", - value = jsonencode(["PY", var.jetbrains_ide_versions["PY"].build_number, "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"]) + icon = "/icon/pycharm.svg", + name = "PyCharm Professional", + identifier = "PY", + build_number = var.jetbrains_ide_versions["PY"].build_number, + download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz" }, "CL" = { - icon = "/icon/clion.svg", - name = "CLion", - value = jsonencode(["CL", var.jetbrains_ide_versions["CL"].build_number, "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"]) + icon = "/icon/clion.svg", + name = "CLion", + identifier = "CL", + build_number = var.jetbrains_ide_versions["CL"].build_number, + download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz" }, "PS" = { - icon = "/icon/phpstorm.svg", - name = "PhpStorm", - value = jsonencode(["PS", var.jetbrains_ide_versions["PS"].build_number, "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"]) + icon = "/icon/phpstorm.svg", + name = "PhpStorm", + identifier = "PS", + build_number = var.jetbrains_ide_versions["PS"].build_number, + download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz" }, "RM" = { - icon = "/icon/rubymine.svg", - name = "RubyMine", - value = jsonencode(["RM", var.jetbrains_ide_versions["RM"].build_number, "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"]) + icon = "/icon/rubymine.svg", + name = "RubyMine", + identifier = "RM", + build_number = var.jetbrains_ide_versions["RM"].build_number, + download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz" } } } data "coder_parameter" "jetbrains_ide" { - type = "list(string)" + type = "string" name = "jetbrains_ide" display_name = "JetBrains IDE" icon = "/icon/gateway.svg" mutable = true - # check if default is in the jet_brains_ides list and if it is not empty or null otherwise set it to null - default = var.default != null && var.default != "" && contains(var.jetbrains_ides, var.default) ? local.jetbrains_ides[var.default].value : local.jetbrains_ides[var.jetbrains_ides[0]].value + default = var.default == "" ? var.jetbrains_ides[0] : var.default dynamic "option" { - for_each = { for key, value in local.jetbrains_ides : key => value if contains(var.jetbrains_ides, key) } + for_each = var.jetbrains_ides content { - icon = option.value.icon - name = option.value.name - value = option.value.value + icon = lookup(local.jetbrains_ides, option.value).icon + name = lookup(local.jetbrains_ides, option.value).name + value = lookup(local.jetbrains_ides, option.value).identifier } } } @@ -168,9 +180,9 @@ data "coder_workspace" "me" {} resource "coder_app" "gateway" { agent_id = var.agent_id - display_name = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].name slug = "gateway" - icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon + display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE") + icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg") external = true url = join("", [ "jetbrains-gateway://connect#type=coder&workspace=", @@ -184,14 +196,38 @@ resource "coder_app" "gateway" { "&token=", "$SESSION_TOKEN", "&ide_product_code=", - jsondecode(data.coder_parameter.jetbrains_ide.value)[0], + local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier, "&ide_build_number=", - jsondecode(data.coder_parameter.jetbrains_ide.value)[1], + local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number, "&ide_download_link=", - jsondecode(data.coder_parameter.jetbrains_ide.value)[2], + local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link ]) } -output "jetbrains_ides" { +output "identifier" { value = data.coder_parameter.jetbrains_ide.value } + +output "name" { + value = coder_app.gateway.display_name +} + +output "icon" { + value = coder_app.gateway.icon +} + +output "download_link" { + value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link +} + +output "build_number" { + value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number +} + +output "version" { + value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version +} + +output "url" { + value = coder_app.gateway.url +} \ No newline at end of file diff --git a/package.json b/package.json index bab18d3..5a73d51 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "test": "bun test", "fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", "fmt:ci": "bun x prettier --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.tf .sample/main.tf", - "lint": "bun run lint.ts", + "lint": "bun run lint.ts && ./terraform_validate.sh", "update-version": "./update-version.sh" }, "devDependencies": { "bun-types": "^1.0.18", "gray-matter": "^4.0.3", - "marked": "^11.1.0", + "marked": "^12.0.0", "prettier-plugin-sh": "^0.13.1", "prettier-plugin-terraform-formatter": "^1.2.1" }, diff --git a/terraform_validate.sh b/terraform_validate.sh new file mode 100755 index 0000000..292c94c --- /dev/null +++ b/terraform_validate.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +# Function to run terraform init and validate in a directory +run_terraform() { + local dir="$1" + echo "Running terraform init and validate in $dir" + pushd "$dir" + terraform init -upgrade + terraform validate + popd +} + +# Main script +main() { + # Get the directory of the script + script_dir=$(dirname "$(readlink -f "$0")") + + # Get all subdirectories in the repository + subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort) + + for dir in $subdirs; do + run_terraform "$dir" + done +} + +# Run the main script +main diff --git a/vault-github/README.md b/vault-github/README.md index d4ad367..ac73972 100644 --- a/vault-github/README.md +++ b/vault-github/README.md @@ -15,7 +15,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec ```tf module "vault" { source = "registry.coder.com/modules/vault-github/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" } @@ -46,7 +46,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t ```tf module "vault" { source = "registry.coder.com/modules/vault-github/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" coder_github_auth_id = "my-github-auth-id" @@ -58,7 +58,7 @@ module "vault" { ```tf module "vault" { source = "registry.coder.com/modules/vault-github/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" coder_github_auth_id = "my-github-auth-id" @@ -71,7 +71,7 @@ module "vault" { ```tf module "vault" { source = "registry.coder.com/modules/vault-github/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_cli_version = "1.15.0" diff --git a/vault-github/main.test.ts b/vault-github/main.test.ts index 91ad50b..25934c8 100644 --- a/vault-github/main.test.ts +++ b/vault-github/main.test.ts @@ -1,7 +1,7 @@ import { describe } from "bun:test"; import { runTerraformInit, testRequiredVariables } from "../test"; -describe("vault-token", async () => { +describe("vault-github", async () => { await runTerraformInit(import.meta.dir); testRequiredVariables(import.meta.dir, { diff --git a/vault-github/run.sh b/vault-github/run.sh index 13f871e..8ca96c0 100644 --- a/vault-github/run.sh +++ b/vault-github/run.sh @@ -32,9 +32,19 @@ unzip_safe() { } install() { + # Get the architecture of the system + ARCH=$(uname -m) + if [ "$${ARCH}" = "x86_64" ]; then + ARCH="amd64" + elif [ "$${ARCH}" = "aarch64" ]; then + ARCH="arm64" + else + printf "Unsupported architecture: $${ARCH}\n" + return 1 + fi # Fetch the latest version of Vault if INSTALL_VERSION is 'latest' if [ "$${INSTALL_VERSION}" = "latest" ]; then - LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v '-rc' | grep -oP 'vault/\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n 1) + LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1) printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}" if [ -z "$${LATEST_VERSION}" ]; then printf "Failed to determine the latest Vault version.\n" @@ -60,7 +70,7 @@ install() { else printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}" fi - fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_amd64.zip" + fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip" if [ $? -ne 0 ]; then printf "Failed to download Vault.\n" return 1 diff --git a/vault-token/README.md b/vault-token/README.md index 32f5185..7e632a5 100644 --- a/vault-token/README.md +++ b/vault-token/README.md @@ -21,7 +21,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/modules/vault-token/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_token = var.token vault_addr = "https://vault.example.com" @@ -74,7 +74,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/modules/vault-token/coder" - version = "1.0.4" + version = "1.0.7" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_token = var.token diff --git a/vault-token/run.sh b/vault-token/run.sh index cb5125d..e1da6ee 100644 --- a/vault-token/run.sh +++ b/vault-token/run.sh @@ -30,9 +30,19 @@ unzip_safe() { } install() { + # Get the architecture of the system + ARCH=$(uname -m) + if [ "$${ARCH}" = "x86_64" ]; then + ARCH="amd64" + elif [ "$${ARCH}" = "aarch64" ]; then + ARCH="arm64" + else + printf "Unsupported architecture: $${ARCH}\n" + return 1 + fi # Fetch the latest version of Vault if INSTALL_VERSION is 'latest' if [ "$${INSTALL_VERSION}" = "latest" ]; then - LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v '-rc' | grep -oP 'vault/\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n 1) + LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1) printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}" if [ -z "$${LATEST_VERSION}" ]; then printf "Failed to determine the latest Vault version.\n" diff --git a/vscode-web/README.md b/vscode-web/README.md index cfc092c..0fefe42 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -9,12 +9,12 @@ tags: [helper, ide, vscode, web] # VS Code Web -Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace using the [VS Code CLI](https://code.visualstudio.com/docs/editor/command-line) and create an app to access it via the dashboard. +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. ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.3" + version = "1.0.6" agent_id = coder_agent.example.id accept_license = true } @@ -29,10 +29,22 @@ module "vscode-web" { ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.3" + version = "1.0.6" agent_id = coder_agent.example.id - install_dir = "/home/coder/.vscode-web" + install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" accept_license = true } ``` + +### Install Extensions + +```tf +module "vscode-web" { + source = "registry.coder.com/modules/vscode-web/coder" + version = "1.0.6" + agent_id = coder_agent.example.id + extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] + accept_license = true +} +``` diff --git a/vscode-web/main.test.ts b/vscode-web/main.test.ts deleted file mode 100644 index 57277df..0000000 --- a/vscode-web/main.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { - executeScriptInContainer, - runTerraformApply, - runTerraformInit, -} from "../test"; - -describe("vscode-web", async () => { - await runTerraformInit(import.meta.dir); - - // replaces testRequiredVariables due to license variable - // may add a testRequiredVariablesWithLicense function later - it("missing agent_id", async () => { - try { - await runTerraformApply(import.meta.dir, { - accept_license: "true", - }); - } catch (ex) { - expect(ex.message).toContain('input variable "agent_id" is not set'); - } - }); - - it("invalid license_agreement", async () => { - try { - await runTerraformApply(import.meta.dir, { - agent_id: "foo", - }); - } catch (ex) { - expect(ex.message).toContain( - "You must accept the VS Code license agreement by setting accept_license=true", - ); - } - }); - - it("fails without curl", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - accept_license: "true", - }); - const output = await executeScriptInContainer(state, "alpine"); - expect(output.exitCode).toBe(1); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling vscode-cli!", - "Failed to install vscode-cli:", // TODO: manually test error log - ]); - }); - - it("runs with curl", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - accept_license: "true", - }); - const output = await executeScriptInContainer(state, "alpine/curl"); - expect(output.exitCode).toBe(0); - expect(output.stdout).toEqual([ - "\u001b[0;1mInstalling vscode-cli!", - "🥳 vscode-cli has been installed.", - "", - "👷 Running /tmp/vscode-cli/bin/code serve-web --port 13338 --without-connection-token --accept-server-license-terms in the background...", - "Check logs at /tmp/vscode-web.log!", - ]); - }); -}); diff --git a/vscode-web/main.tf b/vscode-web/main.tf index 9932fac..640bf4e 100644 --- a/vscode-web/main.tf +++ b/vscode-web/main.tf @@ -53,15 +53,21 @@ variable "log_path" { default = "/tmp/vscode-web.log" } -variable "install_dir" { +variable "install_prefix" { type = string - description = "The directory to install VS Code CLI" - default = "/tmp/vscode-cli" + description = "The prefix to install vscode-web to." + default = "/tmp/vscode-web" +} + +variable "extensions" { + type = list(string) + description = "A list of extensions to install." + default = [] } variable "accept_license" { type = bool - description = "Accept the VS Code license. https://code.visualstudio.com/license" + description = "Accept the VS Code Server license. https://code.visualstudio.com/license/server" default = false validation { condition = var.accept_license == true @@ -69,6 +75,16 @@ variable "accept_license" { } } +variable "telemetry_level" { + type = string + description = "Set the telemetry level for VS Code Web." + default = "error" + validation { + condition = var.telemetry_level == "off" || var.telemetry_level == "crash" || var.telemetry_level == "error" || var.telemetry_level == "all" + error_message = "Incorrect value. Please set either 'off', 'crash', 'error', or 'all'." + } +} + resource "coder_script" "vscode-web" { agent_id = var.agent_id display_name = "VS Code Web" @@ -76,7 +92,9 @@ resource "coder_script" "vscode-web" { script = templatefile("${path.module}/run.sh", { PORT : var.port, LOG_PATH : var.log_path, - INSTALL_DIR : var.install_dir, + INSTALL_PREFIX : var.install_prefix, + EXTENSIONS : join(",", var.extensions), + TELEMETRY_LEVEL : var.telemetry_level, }) run_on_start = true } diff --git a/vscode-web/run.sh b/vscode-web/run.sh old mode 100644 new mode 100755 index d19be62..5977440 --- a/vscode-web/run.sh +++ b/vscode-web/run.sh @@ -1,21 +1,49 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash BOLD='\033[0;1m' +EXTENSIONS=("${EXTENSIONS}") -# Create install directory if it doesn't exist -mkdir -p ${INSTALL_DIR} +# Create install prefix +mkdir -p ${INSTALL_PREFIX} -printf "$${BOLD}Installing vscode-cli!\n" +printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n" -# Download and extract code-cli tarball -output=$(curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64' --output vscode_cli.tar.gz && tar -xf vscode_cli.tar.gz -C ${INSTALL_DIR} && rm vscode_cli.tar.gz) +# Download and extract vscode-server +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH="x64" ;; + aarch64) ARCH="arm64" ;; + *) + echo "Unsupported architecture" + exit 1 + ;; +esac + +HASH=$(curl https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2) +output=$(curl -sL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1) if [ $? -ne 0 ]; then - echo "Failed to install vscode-cli: $output" + echo "Failed to install Microsoft Visual Studio Code Server: $output" exit 1 fi -printf "🥳 vscode-cli has been installed.\n\n" +printf "$${BOLD}Microsoft Visual Studio Code Server has been installed.\n" + +VSCODE_SERVER="${INSTALL_PREFIX}/bin/code-server" + +# Install each extension... +IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" +for extension in "$${EXTENSIONLIST[@]}"; do + if [ -z "$extension" ]; then + continue + fi + printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" + output=$($VSCODE_SERVER --install-extension "$extension" --force) + if [ $? -ne 0 ]; then + echo "Failed to install extension: $extension: $output" + exit 1 + fi +done -echo "👷 Running ${INSTALL_DIR}/bin/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms in the background..." +echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..." echo "Check logs at ${LOG_PATH}!" -${INSTALL_DIR}/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms > ${LOG_PATH} 2>&1 & +"${INSTALL_PREFIX}/bin/code-server" serve-local --port "${PORT}" --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &