diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ee005c..60a760b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,11 +21,14 @@ jobs: with: bun-version: latest - run: bun test - fmt: + pretty: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v1 with: bun-version: latest - - run: bun fmt:ci + - name: Format + run: bun fmt:ci + - name: Lint + run: bun install && bun lint diff --git a/.icons/filebrowser.svg b/.icons/filebrowser.svg new file mode 100644 index 0000000..5e78ecc --- /dev/null +++ b/.icons/filebrowser.svg @@ -0,0 +1,147 @@ + +image/svg+xml + + + + + \ No newline at end of file diff --git a/.images/flyio-basic.png b/.images/flyio-basic.png new file mode 100644 index 0000000..4cd21a2 Binary files /dev/null and b/.images/flyio-basic.png differ diff --git a/.images/flyio-custom.png b/.images/flyio-custom.png new file mode 100644 index 0000000..4ca25a4 Binary files /dev/null and b/.images/flyio-custom.png differ diff --git a/.images/flyio-filtered.png b/.images/flyio-filtered.png new file mode 100644 index 0000000..f7b0711 Binary files /dev/null and b/.images/flyio-filtered.png differ diff --git a/.images/jfrog.png b/.images/jfrog.png new file mode 100644 index 0000000..330dad2 Binary files /dev/null and b/.images/jfrog.png differ diff --git a/.sample/README.md b/.sample/README.md index ebc3e49..387d45b 100644 --- a/.sample/README.md +++ b/.sample/README.md @@ -11,14 +11,14 @@ tags: [helper] - - ```hcl module "MODULE_NAME" { source = "https://registry.coder.com/modules/MODULE_NAME" } ``` + + ## Examples ### Example 1 diff --git a/.sample/main.tf b/.sample/main.tf index 5dcaf01..733f121 100644 --- a/.sample/main.tf +++ b/.sample/main.tf @@ -61,7 +61,7 @@ resource "coder_script" "MODULE_NAME" { LOG_PATH : var.log_path, }) run_on_start = true - run_on_stopt = false + run_on_stop = false } resource "coder_app" "MODULE_NAME" { diff --git a/aws-region/README.md b/aws-region/README.md index 1051cc6..f23b11f 100644 --- a/aws-region/README.md +++ b/aws-region/README.md @@ -12,12 +12,6 @@ tags: [helper, parameter, regions, aws] A parameter with all AWS regions. This allows developers to select the region closest to them. -![AWS Regions](../.images/aws-region.png) - -## Examples - -### Default Region - Customize the preselected parameter value: ```hcl @@ -31,6 +25,10 @@ provider "aws" { } ``` +![AWS Regions](../.images/aws-regions.png) + +## Examples + ### Customize Regions Change the display name and icon for a region: diff --git a/azure-region/README.md b/azure-region/README.md index cb14303..235f58e 100644 --- a/azure-region/README.md +++ b/azure-region/README.md @@ -11,10 +11,6 @@ tags: [helper, parameter, azure, regions] This module adds a parameter with all Azure regions, allowing developers to select the region closest to them. -## Examples - -### Default region - ```hcl module "azure_region" { source = "https://registry.coder.com/modules/azure-region" @@ -26,6 +22,8 @@ resource "azurem_resource_group" "example" { } ``` +## Examples + ### Customize existing regions Change the display name for a region: diff --git a/bun.lockb b/bun.lockb index dfed919..0d30fe9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/code-server/README.md b/code-server/README.md index 9f360e1..302a1ea 100644 --- a/code-server/README.md +++ b/code-server/README.md @@ -22,6 +22,16 @@ module "code-server" { ## Examples +### Pin Versions + +```hcl +module "code-server" { + source = "https://registry.coder.com/modules/code-server" + agent_id = coder_agent.example.id + install_version = "4.8.3" +} +``` + ### Pre-install Extensions Install the Dracula theme from [OpenVSX](https://open-vsx.org/): diff --git a/code-server/main.tf b/code-server/main.tf index 87e3036..0a8e4f4 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -50,11 +50,18 @@ variable "log_path" { default = "/tmp/code-server.log" } +variable "install_version" { + type = string + description = "The version of code-server to install." + 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), PORT : var.port, LOG_PATH : var.log_path, @@ -69,7 +76,7 @@ resource "coder_app" "code-server" { agent_id = var.agent_id slug = "code-server" display_name = "code-server" - url = "http://localhost:${var.port}/?folder=${var.folder}" + url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}" icon = "/icon/code.svg" subdomain = false share = "owner" diff --git a/code-server/run.sh b/code-server/run.sh index 116af8c..3e1a38b 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash EXTENSIONS=("${EXTENSIONS}") BOLD='\033[0;1m' @@ -6,7 +6,16 @@ CODE='\033[36;40;1m' RESET='\033[0m' printf "$${BOLD}Installing code-server!\n" -output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=${INSTALL_PREFIX}) + +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 @@ -29,10 +38,10 @@ for extension in "$${EXTENSIONS[@]}"; do done # Check if the settings file exists... -if [ ! -f ~/.local/share/code-server/User/settings.json ]; then +if [ ! -f ~/.local/share/code-server/Machine/settings.json ]; then echo "⚙️ Creating settings file..." - mkdir -p ~/.local/share/code-server/User - echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json + mkdir -p ~/.local/share/code-server/Machine + echo "${SETTINGS}" > ~/.local/share/code-server/Machine/settings.json fi echo "👷 Running code-server in the background..." diff --git a/filebrowser/README.md b/filebrowser/README.md new file mode 100644 index 0000000..bd1ba07 --- /dev/null +++ b/filebrowser/README.md @@ -0,0 +1,31 @@ +--- +display_name: File Browser +description: A file browser for your workspace +icon: ../.icons/filebrowser.svg +maintainer_github: coder +verified: true +tags: [helper, filebrowser] +--- + +# File Browser + +A file browser for your workspace. + +```hcl +module "filebrowser" { + source = "https://registry.coder.com/modules/filebrowser" + agent_id = coder_agent.example.id +} +``` + +## Examples + +### Serve a specific directory + +```hcl +module "filebrowser" { + source = "https://registry.coder.com/modules/filebrowser" + agent_id = coder_agent.example.id + folder = "/home/coder/project" +} +``` diff --git a/filebrowser/main.tf b/filebrowser/main.tf new file mode 100644 index 0000000..e624004 --- /dev/null +++ b/filebrowser/main.tf @@ -0,0 +1,57 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log filebrowser to." + default = "/tmp/filebrowser.log" +} + +variable "port" { + type = number + description = "The port to run filebrowser on." + default = 13339 +} + +variable "folder" { + type = string + description = "--root value for filebrowser." + default = "~" +} + +resource "coder_script" "filebrowser" { + agent_id = var.agent_id + display_name = "File Browser" + icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port, + FOLDER : var.folder, + LOG_PATH : var.log_path, + }) + run_on_start = true +} + +resource "coder_app" "filebrowser" { + agent_id = var.agent_id + slug = "filebrowser" + display_name = "File Browser" + url = "http://localhost:${var.port}" + icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" + subdomain = true + share = "owner" +} diff --git a/filebrowser/run.sh b/filebrowser/run.sh new file mode 100644 index 0000000..427c864 --- /dev/null +++ b/filebrowser/run.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +BOLD='\033[0;1m' +echo "$${BOLD}Installing filebrowser \n\n" + +curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash + +echo "🥳 Installation comlete! \n\n" + +echo "👷 Starting filebrowser in background... \n\n" + +ROOT_DIR=${FOLDER} +ROOT_DIR=$${ROOT_DIR/\~/$HOME} + +echo "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n" + +echo "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}' \n\n" + +filebrowser --noauth --root $ROOT_DIR --port ${PORT} >${LOG_PATH} 2>&1 & + +echo "📝 Logs at ${LOG_PATH} \n\n" diff --git a/fly-region/README.md b/fly-region/README.md index 7c14f53..cea64f3 100644 --- a/fly-region/README.md +++ b/fly-region/README.md @@ -4,13 +4,59 @@ description: A parameter with human region names and icons icon: ../.icons/fly.svg maintainer_github: coder verified: true -tags: [helper, parameter, fly] +tags: [helper, parameter, fly.io, regions] --- # Fly.io Region -A parameter with all fly.io regions. This allows developers to select the region closest to them. +This module adds Fly.io regions to your Coder template. Regions can be whitelisted using the `regions` argument and given custom names and custom icons with their respective map arguments (`custom_names`, `custom_icons`). + +We can use the simplest format here, only adding a default selection as the `atl` region. + +```hcl +module "fly-region" { + source = "https://registry.coder.com/modules/fly-region" + default = "atl" +} +``` + +![Fly.io Default](../.images/flyio-basic.png) ## Examples -TODO +### Using region whitelist + +The regions argument can be used to display only the desired regions in the Coder parameter. + +```hcl +module "fly-region" { + source = "https://registry.coder.com/modules/fly-region" + default = "ams" + regions = ["ams", "arn", "atl"] +} +``` + +![Fly.io Filtered Regions](../.images/flyio-filtered.png) + +### Using custom icons and names + +Set custom icons and names with their respective maps. + +```hcl +module "fly-region" { + source = "https://registry.coder.com/modules/fly-region" + default = "ams" + custom_icons = { + "ams" = "/emojis/1f90e.png" + } + custom_names = { + "ams" = "We love the Netherlands!" + } +} +``` + +![Fly.io custom icon and name](../.images/flyio-custom.png) + +## Associated template + +Also see the Coder template registry for a [Fly.io template](https://registry.coder.com/templates/fly-docker-image) that provisions workspaces as Fly.io machines. diff --git a/fly-region/main.test.ts b/fly-region/main.test.ts new file mode 100644 index 0000000..86f6bfc --- /dev/null +++ b/fly-region/main.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("fly-region", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, {}); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, {}); + expect(state.outputs.value.value).toBe(""); + }); + + it("customized default", async () => { + const state = await runTerraformApply(import.meta.dir, { + default: "atl", + }); + expect(state.outputs.value.value).toBe("atl"); + }); +}); diff --git a/fly-region/main.tf b/fly-region/main.tf new file mode 100644 index 0000000..ff6a9e3 --- /dev/null +++ b/fly-region/main.tf @@ -0,0 +1,287 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +variable "display_name" { + default = "Fly.io Region" + description = "The display name of the parameter." + type = string +} + +variable "description" { + default = "The region to deploy workspace infrastructure." + description = "The description of the parameter." + type = string +} + +variable "default" { + default = null + description = "The default region to use if no region is specified." + type = string +} + +variable "mutable" { + default = false + description = "Whether the parameter can be changed after creation." + type = bool +} + +variable "custom_names" { + default = {} + description = "A map of custom display names for region IDs." + type = map(string) +} + +variable "custom_icons" { + default = {} + description = "A map of custom icons for region IDs." + type = map(string) +} + +variable "regions" { + default = [] + description = "List of regions to include for region selection." + type = list(string) +} + +locals { + regions = { + "ams" = { + name = "Amsterdam, Netherlands" + gateway = true + paid_only = false + icon = "/emojis/1f1f3-1f1f1.png" + } + "arn" = { + name = "Stockholm, Sweden" + gateway = false + paid_only = false + icon = "/emojis/1f1f8-1f1ea.png" + } + "atl" = { + name = "Atlanta, Georgia (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "bog" = { + name = "Bogotá, Colombia" + gateway = false + paid_only = false + icon = "/emojis/1f1e8-1f1f4.png" + } + "bom" = { + name = "Mumbai, India" + gateway = true + paid_only = true + icon = "/emojis/1f1ee-1f1f3.png" + } + "bos" = { + name = "Boston, Massachusetts (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "cdg" = { + name = "Paris, France" + gateway = true + paid_only = false + icon = "/emojis/1f1eb-1f1f7.png" + } + "den" = { + name = "Denver, Colorado (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "dfw" = { + name = "Dallas, Texas (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "ewr" = { + name = "Secaucus, NJ (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "eze" = { + name = "Ezeiza, Argentina" + gateway = false + paid_only = false + icon = "/emojis/1f1e6-1f1f7.png" + } + "fra" = { + name = "Frankfurt, Germany" + gateway = true + paid_only = true + icon = "/emojis/1f1e9-1f1ea.png" + } + "gdl" = { + name = "Guadalajara, Mexico" + gateway = false + paid_only = false + icon = "/emojis/1f1f2-1f1fd.png" + } + "gig" = { + name = "Rio de Janeiro, Brazil" + gateway = false + paid_only = false + icon = "/emojis/1f1e7-1f1f7.png" + } + "gru" = { + name = "Sao Paulo, Brazil" + gateway = false + paid_only = false + icon = "/emojis/1f1e7-1f1f7.png" + } + "hkg" = { + name = "Hong Kong, Hong Kong" + gateway = true + paid_only = false + icon = "/emojis/1f1ed-1f1f0.png" + } + "iad" = { + name = "Ashburn, Virginia (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "jnb" = { + name = "Johannesburg, South Africa" + gateway = false + paid_only = false + icon = "/emojis/1f1ff-1f1e6.png" + } + "lax" = { + name = "Los Angeles, California (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "lhr" = { + name = "London, United Kingdom" + gateway = true + paid_only = false + icon = "/emojis/1f1ec-1f1e7.png" + } + "mad" = { + name = "Madrid, Spain" + gateway = false + paid_only = false + icon = "/emojis/1f1ea-1f1f8.png" + } + "mia" = { + name = "Miami, Florida (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "nrt" = { + name = "Tokyo, Japan" + gateway = true + paid_only = false + icon = "/emojis/1f1ef-1f1f5.png" + } + "ord" = { + name = "Chicago, Illinois (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "otp" = { + name = "Bucharest, Romania" + gateway = false + paid_only = false + icon = "/emojis/1f1f7-1f1f4.png" + } + "phx" = { + name = "Phoenix, Arizona (US)" + gateway = false + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "qro" = { + name = "Querétaro, Mexico" + gateway = false + paid_only = false + icon = "/emojis/1f1f2-1f1fd.png" + } + "scl" = { + name = "Santiago, Chile" + gateway = true + paid_only = false + icon = "/emojis/1f1e8-1f1f1.png" + } + "sea" = { + name = "Seattle, Washington (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "sin" = { + name = "Singapore, Singapore" + gateway = true + paid_only = false + icon = "/emojis/1f1f8-1f1ec.png" + } + "sjc" = { + name = "San Jose, California (US)" + gateway = true + paid_only = false + icon = "/emojis/1f1fa-1f1f8.png" + } + "syd" = { + name = "Sydney, Australia" + gateway = true + paid_only = false + icon = "/emojis/1f1e6-1f1fa.png" + } + "waw" = { + name = "Warsaw, Poland" + gateway = false + paid_only = false + icon = "/emojis/1f1f5-1f1f1.png" + } + "yul" = { + name = "Montreal, Canada" + gateway = false + paid_only = false + icon = "/emojis/1f1e8-1f1e6.png" + } + "yyz" = { + name = "Toronto, Canada" + gateway = true + paid_only = false + icon = "/emojis/1f1e8-1f1e6.png" + } + } +} + +data "coder_parameter" "fly_region" { + name = "flyio_region" + display_name = var.display_name + description = var.description + default = (var.default != null && var.default != "") && ((var.default != null ? contains(var.regions, var.default) : false) || length(var.regions) == 0) ? var.default : null + mutable = var.mutable + dynamic "option" { + for_each = { for k, v in local.regions : k => v if anytrue([for d in var.regions : k == d]) || length(var.regions) == 0 } + content { + name = try(var.custom_names[option.key], option.value.name) + icon = try(var.custom_icons[option.key], option.value.icon) + value = option.key + } + } +} + +output "value" { + value = data.coder_parameter.fly_region.value +} \ No newline at end of file diff --git a/gcp-region/README.md b/gcp-region/README.md index cfa1370..0ca76e1 100644 --- a/gcp-region/README.md +++ b/gcp-region/README.md @@ -11,38 +11,61 @@ tags: [gcp, regions, parameter, helper] This module adds Google Cloud Platform regions to your Coder template. +```hcl +module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + regions = ["us", "europe"] +} + +resource "google_compute_instance" "example" { + zone = module.gcp_region.value +} +``` + ![GCP Regions](../.images/gcp-regions.png) ## Examples -1. Add only GPU zones in the US West 1 region: - - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - default = ["us-west1-a"] - regions = ["us-west1"] - gpu_only = false - } - ``` - -2. Add all zones in the Europe West region: - - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - regions = ["europe-west"] - single_zone_per_region = false - } - ``` - -3. Add a single zone from each region in US and Europe that laos has GPUs - - ```hcl - module "gcp_region" { - source = "https://registry.coder.com/modules/gcp-region" - regions = ["us", "europe"] - gpu_only = true - single_zone_per_region = true - } - ``` +### Add only GPU zones in the US West 1 region + +```hcl +module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + default = ["us-west1-a"] + regions = ["us-west1"] + gpu_only = false +} + +resource "google_compute_instance" "example" { + zone = module.gcp_region.value +} +``` + +### Add all zones in the Europe West region + +```hcl +module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + regions = ["europe-west"] + single_zone_per_region = false +} + +resource "google_compute_instance" "example" { + zone = module.gcp_region.value +} +``` + +### Add a single zone from each region in US and Europe that laos has GPUs + +```hcl +module "gcp_region" { + source = "https://registry.coder.com/modules/gcp-region" + regions = ["us", "europe"] + gpu_only = true + single_zone_per_region = true +} + +resource "google_compute_instance" "example" { + zone = module.gcp_region.value +} +``` diff --git a/git-clone/README.md b/git-clone/README.md index 40ed231..53e566b 100644 --- a/git-clone/README.md +++ b/git-clone/README.md @@ -13,8 +13,9 @@ This module allows you to automatically clone a repository by URL and skip if it ```hcl module "git-clone" { - source = "https://registry.coder.com/modules/git-clone" - url = "https://github.com/coder/coder" + source = "https://registry.coder.com/modules/git-clone" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" } ``` @@ -32,8 +33,9 @@ data "coder_git_auth" "github" { ```hcl module "git-clone" { - source = "https://registry.coder.com/modules/git-clone" - url = "https://github.com/coder/coder" - path = "~/projects/coder/coder" + source = "https://registry.coder.com/modules/git-clone" + agent_id = coder_agent.example.id + url = "https://github.com/coder/coder" + path = "~/projects/coder/coder" } ``` diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index d7fdfb0..8ac7f41 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -11,6 +11,16 @@ tags: [ide, jetbrains, helper, parameter] This module adds a JetBrains Gateway Button to open any workspace with a single click. +```hcl +module "jetbrains_gateway" { + source = "https://registry.coder.com/modules/jetbrains-gateway" + agent_id = coder_agent.example.id + agent_name = "example" + project_directory = "/home/coder/example" + jetbrains_ides = ["GO", "WS", "IU", "IC", "PY", "PC", "PS", "CL", "RM", "DB", "RD"] +} +``` + ![JetBrains Gateway IDes list](../.images/jetbrains-gateway.png) ## Examples diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index 9e01cf0..4eb2063 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -25,6 +25,7 @@ variable "project_directory" { } variable "default" { + default = null type = string description = "Default IDE" } @@ -38,7 +39,17 @@ variable "jetbrains_ides" { for code in var.jetbrains_ides : contains(["IU", "IC", "PS", "WS", "PY", "PC", "CL", "GO", "DB", "RD", "RM"], code) ]) ) - error_message = "The jetbrains_ides must be a list of valid product codes. https://plugins.jetbrains.com/docs/marketplace/product-codes.html" + error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are: IU, IC, PS, WS, PY, PC, CL, GO, DB, RD, RM." + } + # check if the list is empty + validation { + condition = length(var.jetbrains_ides) > 0 + error_message = "The jetbrains_ides must not be empty." + } + #ccheck if the list contains duplicates + validation { + condition = length(var.jetbrains_ides) == length(toset(var.jetbrains_ides)) + error_message = "The jetbrains_ides must not contain duplicates." } } @@ -108,7 +119,8 @@ data "coder_parameter" "jetbrains_ide" { display_name = "JetBrains IDE" icon = "/icon/gateway.svg" mutable = true - default = var.default != null && var.default != "" ? local.jetbrains_ides[var.default].value : null + # 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 : null dynamic "option" { for_each = { for key, value in local.jetbrains_ides : key => value if contains(var.jetbrains_ides, key) } @@ -126,9 +138,26 @@ 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" - url = "jetbrains-gateway://connect#type=coder&workspace=${data.coder_workspace.me.name}&agent=${var.agent_name}&folder=${var.project_directory}&url=${data.coder_workspace.me.access_url}&token=${data.coder_workspace.me.owner_session_token}&ide_product_code=${jsondecode(data.coder_parameter.jetbrains_ide.value)[0]}&ide_build_number=${jsondecode(data.coder_parameter.jetbrains_ide.value)[1]}&ide_download_link=${jsondecode(data.coder_parameter.jetbrains_ide.value)[2]}" icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon external = true + url = join("", [ + "jetbrains-gateway://connect#type=coder&workspace=", + data.coder_workspace.me.name, + "&agent=", + var.agent_name, + "&folder=", + var.project_directory, + "&url=", + data.coder_workspace.me.access_url, + "&token=", + "$SESSION_TOKEN", + "&ide_product_code=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[0], + "&ide_build_number=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[1], + "&ide_download_link=", + jsondecode(data.coder_parameter.jetbrains_ide.value)[2] + ]) } output "jetbrains_ides" { diff --git a/jfrog/README.md b/jfrog/README.md index f5f9670..9ae7cce 100644 --- a/jfrog/README.md +++ b/jfrog/README.md @@ -10,4 +10,47 @@ tags: [integration] # JFrog -TODO +Install the JF CLI and authenticate package managers with Artifactory. + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog" + agent_id = coder_agent.example.id + jfrog_url = "https://YYYY.jfrog.io" + artifactory_access_token = var.artifactory_access_token # An admin access token + package_managers = { + "npm": "npm-remote", + "go": "go-remote", + "pypi": "pypi-remote" + } +} +``` + +Get a JFrog access token from your Artifactory instance. The token must have admin permissions. It is recommended to store the token in a secret terraform variable. + +```hcl +variable "artifactory_access_token" { + type = string + sensitive = true +} +``` + +![JFrog](../.images/jfrog.png) + +## Examples + +### Configure npm, go, and pypi to use Artifactory local repositories + +```hcl +module "jfrog" { + source = "https://registry.coder.com/modules/jfrog" + agent_id = coder_agent.example.id + jfrog_url = "https://YYYY.jfrog.io" + artifactory_access_token = var.artifactory_access_token # An admin access token + package_managers = { + "npm": "npm-local", + "go": "go-local", + "pypi": "pypi-local" + } +} +``` diff --git a/jfrog/main.test.ts b/jfrog/main.test.ts new file mode 100644 index 0000000..82ad38b --- /dev/null +++ b/jfrog/main.test.ts @@ -0,0 +1,41 @@ +import { serve } from "bun"; +import { describe } from "bun:test"; +import { + createJSONResponse, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("jfrog", async () => { + await runTerraformInit(import.meta.dir); + + // Run a fake JFrog server so the provider can initialize + // correctly. This saves us from having to make remote requests! + const fakeFrogHost = serve({ + fetch: (req) => { + const url = new URL(req.url); + // See https://jfrog.com/help/r/jfrog-rest-apis/license-information + if (url.pathname === "/artifactory/api/system/license") + return createJSONResponse({ + type: "Commercial", + licensedTo: "JFrog inc.", + validThrough: "May 15, 2036", + }); + if (url.pathname === "/access/api/v1/tokens") + return createJSONResponse({ + token_id: "xxx", + access_token: "xxx", + scope: "any", + }); + return createJSONResponse({}); + }, + port: 0, + }); + + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + jfrog_url: "http://" + fakeFrogHost.hostname + ":" + fakeFrogHost.port, + artifactory_access_token: "XXXX", + package_managers: "{}", + }); +}); diff --git a/jfrog/main.tf b/jfrog/main.tf new file mode 100644 index 0000000..807bdf8 --- /dev/null +++ b/jfrog/main.tf @@ -0,0 +1,71 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + artifactory = { + source = "registry.terraform.io/jfrog/artifactory" + version = "~> 8.4.0" + } + } +} + +variable "jfrog_url" { + type = string + description = "JFrog instance URL. e.g. https://YYY.jfrog.io" +} + +variable "artifactory_access_token" { + type = string + description = "The admin-level access token to use for JFrog." +} + +# Configure the Artifactory provider +provider "artifactory" { + url = join("/", [var.jfrog_url, "artifactory"]) + access_token = var.artifactory_access_token +} +resource "artifactory_scoped_token" "me" { + # This is hacky, but on terraform plan the data source gives empty strings, + # which fails validation. + username = length(data.coder_workspace.me.owner_email) > 0 ? data.coder_workspace.me.owner_email : "plan" +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "package_managers" { + type = map(string) + description = < ~/.npmrc +email = ${ARTIFACTORY_USERNAME} +registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} +EOF + jf rt curl /api/npm/auth >> ~/.npmrc +fi + +# Configure the `pip` to use the Artifactory "python" repository. +if [ -z "${REPOSITORY_PYPI}" ]; then + echo "🤔 REPOSITORY_PYPI is not set, skipping pip configuration." +else + echo "🐍 Configuring pip..." + mkdir -p ~/.pip + cat << EOF > ~/.pip/pip.conf +[global] +index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple +EOF +fi + +# Set GOPROXY to use the Artifactory "go" repository. +if [ -z "${REPOSITORY_GO}" ]; then + echo "🤔 REPOSITORY_GO is not set, skipping go configuration." +else + echo "🐹 Configuring go..." + export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}" +fi +echo "🥳 Configuration complete!" \ No newline at end of file diff --git a/lint.ts b/lint.ts new file mode 100644 index 0000000..12e733c --- /dev/null +++ b/lint.ts @@ -0,0 +1,96 @@ +import { readFile, readdir, stat } from "fs/promises"; +import * as path from "path"; +import * as marked from "marked"; +import grayMatter from "gray-matter"; + +const files = await readdir(".", { withFileTypes: true }); +const dirs = files.filter( + (f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules" +); + +let badExit = false; + +// error reports an error to the console and sets badExit to true +// so that the process will exit with a non-zero exit code. +const error = (...data: any[]) => { + console.error(...data); + badExit = true; +} + +// Ensures that each README has the proper format. +// Exits with 0 if all is good! +for (const dir of dirs) { + const readme = path.join(dir.name, "README.md"); + // Ensure exists + try { + await stat(readme); + } catch (ex) { + throw new Error(`Missing README.md in ${dir.name}`); + } + const content = await readFile(readme, "utf8"); + const matter = grayMatter(content); + const data = matter.data as { + display_name?: string; + description?: string; + icon?: string; + maintainer_github?: string; + partner_github?: string; + verified?: boolean; + tags?: string[]; + }; + if (!data.display_name) { + error(dir.name, "missing display_name"); + } + if (!data.description) { + error(dir.name, "missing description"); + } + if (!data.icon) { + error(dir.name, "missing icon"); + } + if (!data.maintainer_github) { + error(dir.name, "missing maintainer_github"); + } + try { + await stat(path.join(".", dir.name, data.icon)); + } catch (ex) { + error(dir.name, "icon does not exist", data.icon); + } + + const tokens = marked.lexer(content); + // Ensure there is an h1 and some text, then a code block + + let h1 = false; + let code = false; + let paragraph = false; + + for (const token of tokens) { + if (token.type === "heading" && token.depth === 1) { + h1 = true; + continue; + } + if (h1 && token.type === "heading") { + break; + } + if (token.type === "paragraph") { + paragraph = true; + continue; + } + if (token.type === "code") { + code = true; + continue; + } + } + if (!h1) { + error(dir.name, "missing h1"); + } + if (!paragraph) { + error(dir.name, "missing paragraph after h1"); + } + if (!code) { + error(dir.name, "missing example code block after paragraph"); + } +} + +if (badExit) { + process.exit(1); +} diff --git a/new.sh b/new.sh index ea80ffe..29e4f52 100755 --- a/new.sh +++ b/new.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # This scripts creates a new sample moduledir with requried files # Run it like : ./new.sh my-module diff --git a/package.json b/package.json index c42b502..c9d3a81 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "scripts": { "test": "bun test", "fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.tf", - "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf" + "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf", + "lint": "bun run lint.ts" }, "devDependencies": { - "bun-types": "^1.0.3" + "bun-types": "^1.0.3", + "gray-matter": "^4.0.3", + "marked": "^9.0.3" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/test.ts b/test.ts index b4c384f..6546490 100644 --- a/test.ts +++ b/test.ts @@ -32,6 +32,7 @@ export const runContainer = async ( export const executeScriptInContainer = async ( state: TerraformState, image: string, + shell: string = "sh", ): Promise<{ exitCode: number; stdout: string[]; @@ -39,7 +40,7 @@ export const executeScriptInContainer = async ( }> => { const instance = findResourceInstance(state, "coder_script"); const id = await runContainer(image); - const resp = await execContainer(id, ["sh", "-c", instance.script]); + const resp = await execContainer(id, [shell, "-c", instance.script]); const stdout = resp.stdout.trim().split("\n"); const stderr = resp.stderr.trim().split("\n"); return { @@ -153,7 +154,7 @@ export const testRequiredVariables = ( await runTerraformApply(dir, localVars); } catch (ex) { expect(ex.message).toContain( - `input variable \"${varName}\" is not set, and has no default`, + `input variable \"${varName}\" is not set`, ); return; } @@ -180,6 +181,7 @@ export const runTerraformApply = async ( "-input=false", "-auto-approve", "-state", + "-no-color", stateFile, ], { @@ -210,3 +212,12 @@ export const runTerraformInit = async (dir: string) => { throw new Error(text); } }; + +export const createJSONResponse = (obj: object, statusCode = 200): Response => { + return new Response(JSON.stringify(obj), { + headers: { + "Content-Type": "application/json", + }, + status: statusCode, + }) +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 86140a5..e7b89cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "esnext", "module": "esnext", + "allowSyntheticDefaultImports": true, + "moduleResolution": "nodenext", "types": ["bun-types"] } } diff --git a/vscode-desktop/main.tf b/vscode-desktop/main.tf index bd7881b..3ef6396 100644 --- a/vscode-desktop/main.tf +++ b/vscode-desktop/main.tf @@ -27,7 +27,6 @@ resource "coder_app" "vscode" { data.coder_workspace.me.owner, "&workspace=", data.coder_workspace.me.name, - "&token=", - data.coder_workspace.me.owner_session_token, + "&token=$SESSION_TOKEN", ]) } diff --git a/vscode-server/README.md b/vscode-server/README.md deleted file mode 100644 index dfbec39..0000000 --- a/vscode-server/README.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -display_name: vscode-server -description: VS Code Web - Visual Studio Code in the browser -icon: ../.icons/code.svg -maintainer_github: coder -verified: true -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 CLIs](https://code.visualstudio.com/docs/editor/command-line) and create an app to access it via the dashboard. - -![VS Code Server with GitHub Copilot and live-share](../.images/vscode-server.gif) - -## Examples - -1. Install VS Code Server with default settings: - - ```hcl - module "vscode-web" { - source = "https://registry.coder.com/modules/vscode-server" - agent_id = coder_agent.example.id - accept_license = true - } - ``` - -2. Install VS Code Server to a custom folder: - - ```hcl - module "vscode-web" { - source = "https://registry.coder.com/modules/vscode-server" - agent_id = coder_agent.example.id - install_dir = "/home/coder/.vscode-server" - folder = "/home/coder" - accept_license = true - } - ``` diff --git a/vscode-web/README.md b/vscode-web/README.md new file mode 100644 index 0000000..4dcfb8d --- /dev/null +++ b/vscode-web/README.md @@ -0,0 +1,36 @@ +--- +display_name: VS Code Web +description: VS Code Web - Visual Studio Code in the browser +icon: ../.icons/code.svg +maintainer_github: coder +verified: true +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. + +```hcl +module "vscode-web" { + source = "https://registry.coder.com/modules/vscode-server" + agent_id = coder_agent.example.id + accept_license = true +} +``` + +![VS Code Server with GitHub Copilot and live-share](../.images/vscode-server.gif) + +## Examples + +### Install VS Code Server to a custom folder + +```hcl +module "vscode-web" { + source = "https://registry.coder.com/modules/vscode-server" + agent_id = coder_agent.example.id + install_dir = "/home/coder/.vscode-server" + folder = "/home/coder" + accept_license = true +} +``` diff --git a/vscode-server/main.tf b/vscode-web/main.tf similarity index 88% rename from vscode-server/main.tf rename to vscode-web/main.tf index bd86703..ef72c52 100644 --- a/vscode-server/main.tf +++ b/vscode-web/main.tf @@ -22,14 +22,14 @@ variable "port" { variable "folder" { type = string - description = "The folder to open in vscode-server." + description = "The folder to open in vscode-web." default = "" } variable "log_path" { type = string description = "The path to log." - default = "/tmp/vscode-server.log" + default = "/tmp/vscode-web.log" } variable "install_dir" { @@ -48,7 +48,7 @@ variable "accept_license" { } } -resource "coder_script" "vscode-server" { +resource "coder_script" "vscode-web" { agent_id = var.agent_id display_name = "VS Code Web" icon = "/icon/code.svg" @@ -60,9 +60,9 @@ resource "coder_script" "vscode-server" { run_on_start = true } -resource "coder_app" "vscode-server" { +resource "coder_app" "vscode-web" { agent_id = var.agent_id - slug = "vscode-server" + slug = "vscode-web" display_name = "VS Code Web" url = var.folder == "" ? "http://localhost:${var.port}" : "http://localhost:${var.port}?folder=${var.folder}" icon = "/icon/code.svg" diff --git a/vscode-server/run.sh b/vscode-web/run.sh similarity index 100% rename from vscode-server/run.sh rename to vscode-web/run.sh