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 @@
+
+
\ 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.
-
-
-## Examples
-
-### Default Region
-
Customize the preselected parameter value:
```hcl
@@ -31,6 +25,10 @@ provider "aws" {
}
```
+
+
+## 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"
+}
+```
+
+
## 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"]
+}
+```
+
+
+
+### 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!"
+ }
+}
+```
+
+
+
+## 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
+}
+```
+

## 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"]
+}
+```
+

## 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
+}
+```
+
+
+
+## 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.
-
-
-
-## 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
+}
+```
+
+
+
+## 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