diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..5ace460
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 960cd03..c5d9c73 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -17,7 +17,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: oven-sh/setup-bun@v1
+ - uses: coder/coder/.github/actions/setup-tf@main
+ - uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup
@@ -27,7 +28,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: oven-sh/setup-bun@v1
+ with:
+ fetch-depth: 0 # Needed to get tags
+ - uses: coder/coder/.github/actions/setup-tf@main
+ - uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup
@@ -38,3 +42,16 @@ jobs:
uses: crate-ci/typos@v1.17.2
- name: Lint
run: bun lint
+ - name: Check version
+ shell: bash
+ run: |
+ # check for version changes
+ ./update-version.sh
+ # Check if any changes were made in README.md files
+ if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then
+ echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files."
+ git diff -- '**/README.md'
+ exit 1
+ else
+ echo "No version mismatch detected. All versions are up to date."
+ fi
diff --git a/.github/workflows/update-readme.yaml b/.github/workflows/update-readme.yaml
deleted file mode 100644
index 0d0e226..0000000
--- a/.github/workflows/update-readme.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Update README on Tag
-
-on:
- workflow_dispatch:
- push:
- tags:
- - 'v*'
-
-jobs:
- update-readme:
- permissions:
- contents: write
- pull-requests: write
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Get the latest tag
- id: get-latest-tag
- run: echo "TAG=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_OUTPUT
-
- - name: Run update script
- run: ./update-version.sh
-
- - name: Create Pull Request
- id: create-pr
- uses: peter-evans/create-pull-request@v5
- with:
- commit-message: 'chore: bump version to ${{ env.TAG }} in README.md files'
- title: 'chore: bump version to ${{ env.TAG }} in README.md files'
- body: 'This is an auto-generated PR to update README.md files of all modules with the new tag ${{ env.TAG }}'
- branch: 'update-readme-branch'
- base: 'main'
- env:
- TAG: ${{ steps.get-latest-tag.outputs.TAG }}
-
- - name: Auto-approve
- uses: hmarr/auto-approve-action@v4
- if: github.ref == 'refs/heads/update-readme-branch'
diff --git a/.icons/cursor.svg b/.icons/cursor.svg
new file mode 100644
index 0000000..c074bf2
--- /dev/null
+++ b/.icons/cursor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/aws-region/main.test.ts b/aws-region/main.test.ts
index 0693e65..06f8e56 100644
--- a/aws-region/main.test.ts
+++ b/aws-region/main.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from "bun:test";
import {
- executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
diff --git a/azure-region/main.test.ts b/azure-region/main.test.ts
index bebc0c9..8adbb48 100644
--- a/azure-region/main.test.ts
+++ b/azure-region/main.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from "bun:test";
import {
- executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
diff --git a/bun.lockb b/bun.lockb
index d9abc98..7576953 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/code-server/README.md b/code-server/README.md
index 3692d71..330661c 100644
--- a/code-server/README.md
+++ b/code-server/README.md
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
}
```
@@ -28,7 +28,7 @@ module "code-server" {
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
@@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.17"
+ version = "1.0.18"
agent_id = coder_agent.example.id
offline = true
}
diff --git a/coder-login/main.test.ts b/coder-login/main.test.ts
index d8fba35..aca4321 100644
--- a/coder-login/main.test.ts
+++ b/coder-login/main.test.ts
@@ -1,10 +1,5 @@
-import { describe, expect, it } from "bun:test";
-import {
- executeScriptInContainer,
- runTerraformApply,
- runTerraformInit,
- testRequiredVariables,
-} from "../test";
+import { describe } from "bun:test";
+import { runTerraformInit, testRequiredVariables } from "../test";
describe("coder-login", async () => {
await runTerraformInit(import.meta.dir);
diff --git a/cursor/README.md b/cursor/README.md
new file mode 100644
index 0000000..c2997be
--- /dev/null
+++ b/cursor/README.md
@@ -0,0 +1,35 @@
+---
+display_name: Cursor IDE
+description: Add a one-click button to launch Cursor IDE
+icon: ../.icons/cursor.svg
+maintainer_github: coder
+verified: true
+tags: [ide, cursor, helper]
+---
+
+# Cursor IDE
+
+Add a button to open any workspace with a single click in Cursor IDE.
+
+Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder).
+
+```tf
+module "cursor" {
+ source = "registry.coder.com/modules/cursor/coder"
+ version = "1.0.19"
+ agent_id = coder_agent.example.id
+}
+```
+
+## Examples
+
+### Open in a specific directory
+
+```tf
+module "cursor" {
+ source = "registry.coder.com/modules/cursor/coder"
+ version = "1.0.19"
+ agent_id = coder_agent.example.id
+ folder = "/home/coder/project"
+}
+```
diff --git a/cursor/main.test.ts b/cursor/main.test.ts
new file mode 100644
index 0000000..3c16469
--- /dev/null
+++ b/cursor/main.test.ts
@@ -0,0 +1,88 @@
+import { describe, expect, it } from "bun:test";
+import {
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "../test";
+
+describe("cursor", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "foo",
+ });
+
+ it("default output", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ });
+ expect(state.outputs.cursor_url.value).toBe(
+ "cursor://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
+ );
+
+ const coder_app = state.resources.find(
+ (res) => res.type === "coder_app" && res.name === "cursor",
+ );
+
+ expect(coder_app).not.toBeNull();
+ expect(coder_app?.instances.length).toBe(1);
+ expect(coder_app?.instances[0].attributes.order).toBeNull();
+ });
+
+ it("adds folder", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ folder: "/foo/bar",
+ });
+ expect(state.outputs.cursor_url.value).toBe(
+ "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
+ );
+ });
+
+ it("adds folder and open_recent", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ folder: "/foo/bar",
+ open_recent: "true",
+ });
+ expect(state.outputs.cursor_url.value).toBe(
+ "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
+ );
+ });
+
+ it("adds folder but not open_recent", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ folder: "/foo/bar",
+ openRecent: "false",
+ });
+ expect(state.outputs.cursor_url.value).toBe(
+ "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
+ );
+ });
+
+ it("adds open_recent", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ open_recent: "true",
+ });
+ expect(state.outputs.cursor_url.value).toBe(
+ "cursor://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
+ );
+ });
+
+ it("expect order to be set", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ order: "22",
+ });
+
+ const coder_app = state.resources.find(
+ (res) => res.type === "coder_app" && res.name === "cursor",
+ );
+
+ expect(coder_app).not.toBeNull();
+ expect(coder_app?.instances.length).toBe(1);
+ expect(coder_app?.instances[0].attributes.order).toBe(22);
+ });
+});
diff --git a/cursor/main.tf b/cursor/main.tf
new file mode 100644
index 0000000..f350f94
--- /dev/null
+++ b/cursor/main.tf
@@ -0,0 +1,62 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 0.23"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "folder" {
+ type = string
+ description = "The folder to open in Cursor IDE."
+ default = ""
+}
+
+variable "open_recent" {
+ type = bool
+ description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
+ default = false
+}
+
+variable "order" {
+ type = number
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
+ default = null
+}
+
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
+resource "coder_app" "cursor" {
+ agent_id = var.agent_id
+ external = true
+ icon = "/icon/cursor.svg"
+ slug = "cursor"
+ display_name = "Cursor Desktop"
+ order = var.order
+ url = join("", [
+ "cursor://coder.coder-remote/open",
+ "?owner=",
+ data.coder_workspace_owner.me.name,
+ "&workspace=",
+ data.coder_workspace.me.name,
+ var.folder != "" ? join("", ["&folder=", var.folder]) : "",
+ var.open_recent ? "&openRecent" : "",
+ "&url=",
+ data.coder_workspace.me.access_url,
+ "&token=$SESSION_TOKEN",
+ ])
+}
+
+output "cursor_url" {
+ value = coder_app.cursor.url
+ description = "Cursor IDE Desktop URL."
+}
diff --git a/dotfiles/README.md b/dotfiles/README.md
index 41371ab..6d7673c 100644
--- a/dotfiles/README.md
+++ b/dotfiles/README.md
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "dotfiles" {
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
}
```
@@ -40,7 +40,7 @@ module "dotfiles" {
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
user = "root"
}
@@ -51,13 +51,13 @@ module "dotfiles" {
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
@@ -71,7 +71,7 @@ You can set a default dotfiles repository for all users by setting the `default_
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
- version = "1.0.15"
+ version = "1.0.18"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
diff --git a/dotfiles/main.tf b/dotfiles/main.tf
index bfb67e4..9bc3735 100644
--- a/dotfiles/main.tf
+++ b/dotfiles/main.tf
@@ -39,9 +39,14 @@ variable "coder_parameter_order" {
default = null
}
-data "coder_parameter" "dotfiles_uri" {
- count = var.dotfiles_uri == null ? 1 : 0
+variable "manual_update" {
+ type = bool
+ description = "If true, this adds a button to workspace page to refresh dotfiles on demand."
+ default = false
+}
+data "coder_parameter" "dotfiles_uri" {
+ count = var.dotfiles_uri == null ? 1 : 0
type = "string"
name = "dotfiles_uri"
display_name = "Dotfiles URL"
@@ -68,6 +73,18 @@ resource "coder_script" "dotfiles" {
run_on_start = true
}
+resource "coder_app" "dotfiles" {
+ count = var.manual_update ? 1 : 0
+ agent_id = var.agent_id
+ display_name = "Refresh Dotfiles"
+ slug = "dotfiles"
+ icon = "/icon/dotfiles.svg"
+ command = templatefile("${path.module}/run.sh", {
+ DOTFILES_URI : local.dotfiles_uri,
+ DOTFILES_USER : local.user
+ })
+}
+
output "dotfiles_uri" {
description = "Dotfiles URI"
value = local.dotfiles_uri
diff --git a/exoscale-zone/main.test.ts b/exoscale-zone/main.test.ts
index ca8eeb7..1751cb1 100644
--- a/exoscale-zone/main.test.ts
+++ b/exoscale-zone/main.test.ts
@@ -1,6 +1,5 @@
import { describe, expect, it } from "bun:test";
import {
- executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
diff --git a/filebrowser/README.md b/filebrowser/README.md
index 2881376..1b53ffa 100644
--- a/filebrowser/README.md
+++ b/filebrowser/README.md
@@ -14,7 +14,7 @@ A file browser for your workspace.
```tf
module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder"
- version = "1.0.8"
+ version = "1.0.22"
agent_id = coder_agent.example.id
}
```
@@ -28,7 +28,7 @@ module "filebrowser" {
```tf
module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder"
- version = "1.0.8"
+ version = "1.0.22"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -39,8 +39,19 @@ module "filebrowser" {
```tf
module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder"
- version = "1.0.8"
+ version = "1.0.22"
agent_id = coder_agent.example.id
database_path = ".config/filebrowser.db"
}
```
+
+### Serve from the same domain (no subdomain)
+
+```tf
+module "filebrowser" {
+ source = "registry.coder.com/modules/filebrowser/coder"
+ agent_id = coder_agent.example.id
+ agent_name = "main"
+ subdomain = false
+}
+```
diff --git a/filebrowser/main.test.ts b/filebrowser/main.test.ts
index 79dd99d..7dd4972 100644
--- a/filebrowser/main.test.ts
+++ b/filebrowser/main.test.ts
@@ -88,4 +88,27 @@ describe("filebrowser", async () => {
"📝 Logs at /tmp/filebrowser.log",
]);
});
+
+ it("runs with subdomain=false", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ agent_name: "main",
+ subdomain: false,
+ });
+ const output = await executeScriptInContainer(state, "alpine");
+ expect(output.exitCode).toBe(0);
+ expect(output.stdout).toEqual([
+ "\u001B[0;1mInstalling filebrowser ",
+ "",
+ "🥳 Installation complete! ",
+ "",
+ "👷 Starting filebrowser in background... ",
+ "",
+ "📂 Serving /root at http://localhost:13339 ",
+ "",
+ "Running 'filebrowser --noauth --root /root --port 13339' ",
+ "",
+ "📝 Logs at /tmp/filebrowser.log",
+ ]);
+ });
});
diff --git a/filebrowser/main.tf b/filebrowser/main.tf
index a07072b..4fd7459 100644
--- a/filebrowser/main.tf
+++ b/filebrowser/main.tf
@@ -14,6 +14,21 @@ variable "agent_id" {
description = "The ID of a Coder agent."
}
+data "coder_workspace" "me" {}
+
+data "coder_workspace_owner" "me" {}
+
+variable "agent_name" {
+ type = string
+ description = "The name of the main deployment. (Used to build the subpath for coder_app.)"
+ default = ""
+ validation {
+ # If subdomain is false, then agent_name must be set.
+ condition = var.subdomain || var.agent_name != ""
+ error_message = "The agent_name must be set."
+ }
+}
+
variable "database_path" {
type = string
description = "The path to the filebrowser database."
@@ -58,6 +73,15 @@ variable "order" {
default = null
}
+variable "subdomain" {
+ type = bool
+ description = <<-EOT
+ Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
+ If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
+ EOT
+ default = true
+}
+
resource "coder_script" "filebrowser" {
agent_id = var.agent_id
display_name = "File Browser"
@@ -67,7 +91,9 @@ resource "coder_script" "filebrowser" {
PORT : var.port,
FOLDER : var.folder,
LOG_PATH : var.log_path,
- DB_PATH : var.database_path
+ DB_PATH : var.database_path,
+ SUBDOMAIN : var.subdomain,
+ SERVER_BASE_PATH : var.subdomain ? "" : format("/@%s/%s.%s/apps/filebrowser", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name),
})
run_on_start = true
}
@@ -78,7 +104,7 @@ resource "coder_app" "filebrowser" {
display_name = "File Browser"
url = "http://localhost:${var.port}"
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
- subdomain = true
+ subdomain = var.subdomain
share = var.share
order = var.order
}
diff --git a/filebrowser/run.sh b/filebrowser/run.sh
index 8744edb..8a31d4d 100644
--- a/filebrowser/run.sh
+++ b/filebrowser/run.sh
@@ -17,6 +17,9 @@ if [ "${DB_PATH}" != "filebrowser.db" ]; then
DB_FLAG=" -d ${DB_PATH}"
fi
+# set baseurl to be able to run if sudomain=false; if subdomain=true the SERVER_BASE_PATH value will be ""
+filebrowser config set --baseurl "${SERVER_BASE_PATH}"$${DB_FLAG} > ${LOG_PATH} 2>&1
+
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
diff --git a/git-clone/README.md b/git-clone/README.md
index 255b3f1..6b8871e 100644
--- a/git-clone/README.md
+++ b/git-clone/README.md
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -27,7 +27,7 @@ module "git-clone" {
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -41,7 +41,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -66,7 +66,7 @@ data "coder_parameter" "git_repo" {
# Clone the repository for branch `feat/example`
module "git_clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
@@ -74,7 +74,7 @@ module "git_clone" {
# Create a code-server instance for the cloned repository
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone.folder_name}"
@@ -98,7 +98,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
@@ -116,7 +116,7 @@ To GitLab clone with a specific branch like `feat/example`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -127,7 +127,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
@@ -147,9 +147,26 @@ For example, to clone the `feat/example` branch:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
- version = "1.0.12"
+ version = "1.0.18"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
}
```
+
+## Git clone with different destination folder
+
+By default, the repository will be cloned into a folder matching the repository name. You can use the `folder_name` attribute to change the name of the destination folder to something else.
+
+For example, this will clone into the `~/projects/coder/coder-dev` folder:
+
+```tf
+module "git-clone" {
+ source = "registry.coder.com/modules/git-clone/coder"
+ version = "1.0.18"
+ agent_id = coder_agent.example.id
+ url = "https://github.com/coder/coder"
+ folder_name = "coder-dev"
+ base_dir = "~/projects/coder"
+}
+```
diff --git a/git-clone/main.test.ts b/git-clone/main.test.ts
index 87b0e4a..9fbd202 100644
--- a/git-clone/main.test.ts
+++ b/git-clone/main.test.ts
@@ -79,6 +79,22 @@ describe("git-clone", async () => {
expect(state.outputs.branch_name.value).toEqual("");
});
+ it("repo_dir should match base_dir/folder_name", async () => {
+ const url = "git@github.com:coder/coder.git";
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ base_dir: "/tmp",
+ folder_name: "foo",
+ url,
+ });
+ expect(state.outputs.repo_dir.value).toEqual("/tmp/foo");
+ expect(state.outputs.folder_name.value).toEqual("foo");
+ expect(state.outputs.clone_url.value).toEqual(url);
+ const https_url = "https://github.com/coder/coder.git";
+ expect(state.outputs.web_url.value).toEqual(https_url);
+ expect(state.outputs.branch_name.value).toEqual("");
+ });
+
it("branch_name should not include query string", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
diff --git a/git-clone/main.tf b/git-clone/main.tf
index 4af5000..0295444 100644
--- a/git-clone/main.tf
+++ b/git-clone/main.tf
@@ -50,6 +50,12 @@ variable "branch_name" {
default = ""
}
+variable "folder_name" {
+ description = "The destination folder to clone the repository into."
+ type = string
+ default = ""
+}
+
locals {
# Remove query parameters and fragments from the URL
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
@@ -64,7 +70,7 @@ locals {
# Extract the branch name from the URL
branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name
# Extract the folder name from the URL
- folder_name = replace(basename(local.clone_url), ".git", "")
+ folder_name = var.folder_name == "" ? replace(basename(local.clone_url), ".git", "") : var.folder_name
# Construct the path to clone the repository
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
# Construct the web URL
diff --git a/git-commit-signing/README.md b/git-commit-signing/README.md
index 37633a2..942f2f3 100644
--- a/git-commit-signing/README.md
+++ b/git-commit-signing/README.md
@@ -2,8 +2,8 @@
display_name: Git commit signing
description: Configures Git to sign commits using your Coder SSH key
icon: ../.icons/git.svg
-maintainer_github: phorcys420
-verified: false
+maintainer_github: coder
+verified: true
tags: [helper, git]
---
diff --git a/github-upload-public-key/main.test.ts b/github-upload-public-key/main.test.ts
index fb1b977..6ce16d8 100644
--- a/github-upload-public-key/main.test.ts
+++ b/github-upload-public-key/main.test.ts
@@ -1,3 +1,4 @@
+import { type Server, serve } from "bun";
import { describe, expect, it } from "bun:test";
import {
createJSONResponse,
@@ -9,7 +10,6 @@ import {
testRequiredVariables,
writeCoder,
} from "../test";
-import { Server, serve } from "bun";
describe("github-upload-public-key", async () => {
await runTerraformInit(import.meta.dir);
@@ -21,10 +21,12 @@ describe("github-upload-public-key", async () => {
it("creates new key if one does not exist", async () => {
const { instance, id, server } = await setupContainer();
await writeCoder(id, "echo foo");
- let exec = await execContainer(id, [
+
+ const url = server.url.toString().slice(0, -1);
+ const exec = await execContainer(id, [
"env",
- "CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
- "GITHUB_API_URL=" + server.url.toString().slice(0, -1),
+ `CODER_ACCESS_URL=${url}`,
+ `GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
@@ -42,10 +44,12 @@ describe("github-upload-public-key", async () => {
const { instance, id, server } = await setupContainer();
// use keyword to make server return a existing key
await writeCoder(id, "echo findkey");
- let exec = await execContainer(id, [
+
+ const url = server.url.toString().slice(0, -1);
+ const exec = await execContainer(id, [
"env",
- "CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
- "GITHUB_API_URL=" + server.url.toString().slice(0, -1),
+ `CODER_ACCESS_URL=${url}`,
+ `GITHUB_API_URL=${url}`,
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
@@ -95,7 +99,7 @@ const setupServer = async (): Promise => {
}
// case: key already exists
- if (req.headers.get("Authorization") == "Bearer findkey") {
+ if (req.headers.get("Authorization") === "Bearer findkey") {
return createJSONResponse([
{
key: "foo",
diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md
index b2c0e0f..0745fa7 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.13"
+ version = "1.0.21"
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.13"
+ version = "1.0.21"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
@@ -46,7 +46,7 @@ module "jetbrains_gateway" {
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
- version = "1.0.13"
+ version = "1.0.21"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
@@ -61,7 +61,7 @@ module "jetbrains_gateway" {
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
- version = "1.0.13"
+ version = "1.0.21"
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 b327e41..0a5b3bc 100644
--- a/jetbrains-gateway/main.test.ts
+++ b/jetbrains-gateway/main.test.ts
@@ -14,6 +14,26 @@ describe("jetbrains-gateway", async () => {
folder: "/home/foo",
});
+ it("should create a link with the default values", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ // These are all required.
+ agent_id: "foo",
+ agent_name: "foo",
+ folder: "/home/coder",
+ });
+ expect(state.outputs.url.value).toBe(
+ "jetbrains-gateway://connect#type=coder&workspace=default&owner=default&agent=foo&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=241.14494.240&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.1.tar.gz",
+ );
+
+ const coder_app = state.resources.find(
+ (res) => res.type === "coder_app" && res.name === "gateway",
+ );
+
+ expect(coder_app).not.toBeNull();
+ expect(coder_app?.instances.length).toBe(1);
+ expect(coder_app?.instances[0].attributes.order).toBeNull();
+ });
+
it("default to first ide", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf
index c96098c..2bc00d3 100644
--- a/jetbrains-gateway/main.tf
+++ b/jetbrains-gateway/main.tf
@@ -18,6 +18,12 @@ variable "agent_id" {
description = "The ID of a Coder agent."
}
+variable "slug" {
+ type = string
+ description = "The slug for the coder_app. Allows resuing the module with the same template."
+ default = "gateway"
+}
+
variable "agent_name" {
type = string
description = "Agent name."
@@ -243,10 +249,11 @@ data "coder_parameter" "jetbrains_ide" {
}
data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
resource "coder_app" "gateway" {
agent_id = var.agent_id
- slug = "gateway"
+ slug = var.slug
display_name = local.display_name
icon = local.icon
external = true
@@ -254,6 +261,8 @@ resource "coder_app" "gateway" {
url = join("", [
"jetbrains-gateway://connect#type=coder&workspace=",
data.coder_workspace.me.name,
+ "&owner=",
+ data.coder_workspace_owner.me.name,
"&agent=",
var.agent_name,
"&folder=",
diff --git a/jfrog-oauth/.npmrc.tftpl b/jfrog-oauth/.npmrc.tftpl
new file mode 100644
index 0000000..8bb9fb8
--- /dev/null
+++ b/jfrog-oauth/.npmrc.tftpl
@@ -0,0 +1,5 @@
+email=${ARTIFACTORY_EMAIL}
+%{ for REPO in REPOS ~}
+${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME}
+//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}
+%{ endfor ~}
diff --git a/jfrog-oauth/README.md b/jfrog-oauth/README.md
index b7f9d58..4423a74 100644
--- a/jfrog-oauth/README.md
+++ b/jfrog-oauth/README.md
@@ -17,15 +17,16 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
package_managers = {
- "npm" : "npm",
- "go" : "go",
- "pypi" : "pypi"
+ npm = ["npm", "@scoped:npm-scoped"]
+ go = ["go", "another-go-repo"]
+ pypi = ["pypi", "extra-index-pypi"]
+ docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
}
}
```
@@ -44,13 +45,13 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
package_managers = {
- "pypi" : "pypi"
+ pypi = ["pypi"]
}
}
```
@@ -72,15 +73,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = {
- "npm" : "npm",
- "go" : "go",
- "pypi" : "pypi"
+ npm = ["npm"]
+ go = ["go"]
+ pypi = ["pypi"]
}
}
```
diff --git a/jfrog-oauth/main.test.ts b/jfrog-oauth/main.test.ts
index 3397eeb..7b0c1a5 100644
--- a/jfrog-oauth/main.test.ts
+++ b/jfrog-oauth/main.test.ts
@@ -1,19 +1,129 @@
-import { serve } from "bun";
-import { describe } from "bun:test";
+import { describe, expect, it } from "bun:test";
import {
- createJSONResponse,
+ findResourceInstance,
runTerraformInit,
+ runTerraformApply,
testRequiredVariables,
} from "../test";
describe("jfrog-oauth", async () => {
+ type TestVariables = {
+ agent_id: string;
+ jfrog_url: string;
+ package_managers: string;
+
+ username_field?: string;
+ jfrog_server_id?: string;
+ external_auth_id?: string;
+ configure_code_server?: boolean;
+ };
+
await runTerraformInit(import.meta.dir);
- testRequiredVariables(import.meta.dir, {
- agent_id: "some-agent-id",
- jfrog_url: "http://localhost:8081",
- package_managers: "{}",
+ const fakeFrogApi = "localhost:8081/artifactory/api";
+ const fakeFrogUrl = "http://localhost:8081";
+ const user = "default";
+
+ it("can run apply with required variables", async () => {
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ package_managers: "{}",
+ });
});
-});
-//TODO add more tests
+ it("generates an npmrc with scoped repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ package_managers: JSON.stringify({
+ npm: ["global", "@foo:foo", "@bar:bar"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const npmrcStanza = `cat << EOF > ~/.npmrc
+email=${user}@example.com
+registry=http://${fakeFrogApi}/npm/global
+//${fakeFrogApi}/npm/global/:_authToken=
+@foo:registry=http://${fakeFrogApi}/npm/foo
+//${fakeFrogApi}/npm/foo/:_authToken=
+@bar:registry=http://${fakeFrogApi}/npm/bar
+//${fakeFrogApi}/npm/bar/:_authToken=
+
+EOF`;
+ expect(coderScript.script).toContain(npmrcStanza);
+ expect(coderScript.script).toContain(
+ 'jf npmc --global --repo-resolve "global"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured npm',
+ );
+ });
+
+ it("generates a pip config with extra-indexes", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ package_managers: JSON.stringify({
+ pypi: ["global", "foo", "bar"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const pipStanza = `cat << EOF > ~/.pip/pip.conf
+[global]
+index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
+extra-index-url =
+ https://${user}:@${fakeFrogApi}/pypi/foo/simple
+ https://${user}:@${fakeFrogApi}/pypi/bar/simple
+
+EOF`;
+ expect(coderScript.script).toContain(pipStanza);
+ expect(coderScript.script).toContain(
+ 'jf pipc --global --repo-resolve "global"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured pypi',
+ );
+ });
+
+ it("registers multiple docker repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ package_managers: JSON.stringify({
+ docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const dockerStanza = ["foo", "bar", "baz"]
+ .map((r) => `register_docker "${r}.jfrog.io"`)
+ .join("\n");
+ expect(coderScript.script).toContain(dockerStanza);
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured docker',
+ );
+ });
+
+ it("sets goproxy with multiple repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ package_managers: JSON.stringify({
+ go: ["foo", "bar", "baz"],
+ }),
+ });
+ const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
+ const proxies = ["foo", "bar", "baz"]
+ .map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
+ .join(",");
+ expect(proxyEnv.value).toEqual(proxies);
+
+ const coderScript = findResourceInstance(state, "coder_script");
+ expect(coderScript.script).toContain(
+ 'jf goc --global --repo-resolve "foo"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured go',
+ );
+ });
+});
diff --git a/jfrog-oauth/main.tf b/jfrog-oauth/main.tf
index 767235a..0bc2256 100644
--- a/jfrog-oauth/main.tf
+++ b/jfrog-oauth/main.tf
@@ -53,23 +53,51 @@ variable "configure_code_server" {
}
variable "package_managers" {
- type = map(string)
- description = < /dev/null 2>&1; then
echo "✅ JFrog CLI is already installed, skipping installation."
@@ -20,52 +35,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
-if [ -z "${REPOSITORY_NPM}" ]; then
- echo "🤔 no npm repository is set, skipping npm configuration."
- echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
+if [ -z "${HAS_NPM}" ]; then
+ not_configured npm
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
cat << EOF > ~/.npmrc
-email=${ARTIFACTORY_EMAIL}
-registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
+${NPMRC}
EOF
- echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
+ config_complete
fi
# Configure the `pip` to use the Artifactory "python" repository.
-if [ -z "${REPOSITORY_PYPI}" ]; then
- echo "🤔 no pypi repository is set, skipping pip configuration."
- echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
+if [ -z "${HAS_PYPI}" ]; then
+ not_configured pypi
else
- echo "📦 Configuring pip..."
+ echo "🐍 Configuring pip..."
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
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
+${PIP_CONF}
EOF
+ config_complete
fi
# Configure Artifactory "go" repository.
-if [ -z "${REPOSITORY_GO}" ]; then
- echo "🤔 no go repository is set, skipping go configuration."
- echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
+if [ -z "${HAS_GO}" ]; then
+ not_configured go
else
echo "🐹 Configuring go..."
jf goc --global --repo-resolve "${REPOSITORY_GO}"
+ config_complete
fi
-echo "🥳 Configuration complete!"
# Configure the JFrog CLI to use the Artifactory "docker" repository.
-if [ -z "${REPOSITORY_DOCKER}" ]; then
- echo "🤔 no docker repository is set, skipping docker configuration."
- echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
+if [ -z "${HAS_DOCKER}" ]; then
+ not_configured docker
else
if command -v docker > /dev/null 2>&1; then
echo "🔑 Configuring 🐳 docker credentials..."
mkdir -p ~/.docker
- echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
+ ${REGISTER_DOCKER}
else
echo "🤔 no docker is installed, skipping docker configuration."
fi
@@ -96,20 +106,19 @@ echo "📦 Configuring JFrog CLI completion..."
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
# Generate the completion script
jf completion $SHELLNAME --install
+begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)"
# Add the completion script to the user's shell profile
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
- if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
- echo "" >> ~/.bashrc
- echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.bashrc
+ if ! grep -q "$begin_stanza" ~/.bashrc; then
+ printf "%s\n" "$begin_stanza" >> ~/.bashrc
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
echo "# END: jf CLI shell completion" >> ~/.bashrc
else
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
fi
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
- if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
- echo "" >> ~/.zshrc
- echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.zshrc
+ if ! grep -q "$begin_stanza" ~/.zshrc; then
+ printf "\n%s\n" "$begin_stanza" >> ~/.zshrc
echo "autoload -Uz compinit" >> ~/.zshrc
echo "compinit" >> ~/.zshrc
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
diff --git a/jfrog-token/.npmrc.tftpl b/jfrog-token/.npmrc.tftpl
new file mode 100644
index 0000000..8bb9fb8
--- /dev/null
+++ b/jfrog-token/.npmrc.tftpl
@@ -0,0 +1,5 @@
+email=${ARTIFACTORY_EMAIL}
+%{ for REPO in REPOS ~}
+${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME}
+//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}
+%{ endfor ~}
diff --git a/jfrog-token/README.md b/jfrog-token/README.md
index f903f90..146dc7f 100644
--- a/jfrog-token/README.md
+++ b/jfrog-token/README.md
@@ -15,14 +15,15 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
package_managers = {
- "npm" : "npm",
- "go" : "go",
- "pypi" : "pypi"
+ npm = ["npm", "@scoped:npm-scoped"]
+ go = ["go", "another-go-repo"]
+ pypi = ["pypi", "extra-index-pypi"]
+ docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
}
}
```
@@ -41,14 +42,14 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
- version = "1.0.15"
+ version = "1.0.19"
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"
+ npm = ["npm-local"]
+ go = ["go-local"]
+ pypi = ["pypi-local"]
}
}
```
@@ -74,15 +75,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = {
- "npm" : "npm",
- "go" : "go",
- "pypi" : "pypi"
+ npm = ["npm"]
+ go = ["go"]
+ pypi = ["pypi"]
}
}
```
@@ -94,15 +95,13 @@ data "coder_workspace" "me" {}
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
- version = "1.0.15"
+ version = "1.0.19"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}"
package_managers = {
- "npm" : "npm",
- "go" : "go",
- "pypi" : "pypi"
+ npm = ["npm"]
}
}
```
diff --git a/jfrog-token/main.test.ts b/jfrog-token/main.test.ts
index b3b8df9..2c85672 100644
--- a/jfrog-token/main.test.ts
+++ b/jfrog-token/main.test.ts
@@ -1,12 +1,29 @@
import { serve } from "bun";
-import { describe } from "bun:test";
+import { describe, expect, it } from "bun:test";
import {
createJSONResponse,
+ findResourceInstance,
runTerraformInit,
+ runTerraformApply,
testRequiredVariables,
} from "../test";
describe("jfrog-token", async () => {
+ type TestVariables = {
+ agent_id: string;
+ jfrog_url: string;
+ artifactory_access_token: string;
+ package_managers: string;
+
+ token_description?: string;
+ check_license?: boolean;
+ refreshable?: boolean;
+ expires_in?: number;
+ username_field?: string;
+ jfrog_server_id?: string;
+ configure_code_server?: boolean;
+ };
+
await runTerraformInit(import.meta.dir);
// Run a fake JFrog server so the provider can initialize
@@ -32,10 +49,116 @@ describe("jfrog-token", async () => {
port: 0,
});
- testRequiredVariables(import.meta.dir, {
- agent_id: "some-agent-id",
- jfrog_url: "http://" + fakeFrogHost.hostname + ":" + fakeFrogHost.port,
- artifactory_access_token: "XXXX",
- package_managers: "{}",
+ const fakeFrogApi = `${fakeFrogHost.hostname}:${fakeFrogHost.port}/artifactory/api`;
+ const fakeFrogUrl = `http://${fakeFrogHost.hostname}:${fakeFrogHost.port}`;
+ const user = "default";
+ const token = "xxx";
+
+ it("can run apply with required variables", async () => {
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ artifactory_access_token: "XXXX",
+ package_managers: "{}",
+ });
+ });
+
+ it("generates an npmrc with scoped repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ artifactory_access_token: "XXXX",
+ package_managers: JSON.stringify({
+ npm: ["global", "@foo:foo", "@bar:bar"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const npmrcStanza = `cat << EOF > ~/.npmrc
+email=${user}@example.com
+registry=http://${fakeFrogApi}/npm/global
+//${fakeFrogApi}/npm/global/:_authToken=xxx
+@foo:registry=http://${fakeFrogApi}/npm/foo
+//${fakeFrogApi}/npm/foo/:_authToken=xxx
+@bar:registry=http://${fakeFrogApi}/npm/bar
+//${fakeFrogApi}/npm/bar/:_authToken=xxx
+
+EOF`;
+ expect(coderScript.script).toContain(npmrcStanza);
+ expect(coderScript.script).toContain(
+ 'jf npmc --global --repo-resolve "global"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured npm',
+ );
+ });
+
+ it("generates a pip config with extra-indexes", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ artifactory_access_token: "XXXX",
+ package_managers: JSON.stringify({
+ pypi: ["global", "foo", "bar"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const pipStanza = `cat << EOF > ~/.pip/pip.conf
+[global]
+index-url = https://${user}:${token}@${fakeFrogApi}/pypi/global/simple
+extra-index-url =
+ https://${user}:${token}@${fakeFrogApi}/pypi/foo/simple
+ https://${user}:${token}@${fakeFrogApi}/pypi/bar/simple
+
+EOF`;
+ expect(coderScript.script).toContain(pipStanza);
+ expect(coderScript.script).toContain(
+ 'jf pipc --global --repo-resolve "global"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured pypi',
+ );
+ });
+
+ it("registers multiple docker repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ artifactory_access_token: "XXXX",
+ package_managers: JSON.stringify({
+ docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
+ }),
+ });
+ const coderScript = findResourceInstance(state, "coder_script");
+ const dockerStanza = ["foo", "bar", "baz"]
+ .map((r) => `register_docker "${r}.jfrog.io"`)
+ .join("\n");
+ expect(coderScript.script).toContain(dockerStanza);
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured docker',
+ );
+ });
+
+ it("sets goproxy with multiple repos", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "some-agent-id",
+ jfrog_url: fakeFrogUrl,
+ artifactory_access_token: "XXXX",
+ package_managers: JSON.stringify({
+ go: ["foo", "bar", "baz"],
+ }),
+ });
+ const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
+ const proxies = ["foo", "bar", "baz"]
+ .map((r) => `https://${user}:${token}@${fakeFrogApi}/go/${r}`)
+ .join(",");
+ expect(proxyEnv.value).toEqual(proxies);
+
+ const coderScript = findResourceInstance(state, "coder_script");
+ expect(coderScript.script).toContain(
+ 'jf goc --global --repo-resolve "foo"',
+ );
+ expect(coderScript.script).toContain(
+ 'if [ -z "YES" ]; then\n not_configured go',
+ );
});
});
diff --git a/jfrog-token/main.tf b/jfrog-token/main.tf
index 90dad61..f6f5f5b 100644
--- a/jfrog-token/main.tf
+++ b/jfrog-token/main.tf
@@ -80,23 +80,51 @@ variable "configure_code_server" {
}
variable "package_managers" {
- type = map(string)
- description = < /dev/null 2>&1; then
echo "✅ JFrog CLI is already installed, skipping installation."
@@ -11,8 +26,7 @@ else
sudo chmod 755 /usr/local/bin/jf
fi
-# The jf CLI checks $CI when determining whether to use interactive
-# flows.
+# The jf CLI checks $CI when determining whether to use interactive flows.
export CI=true
# Authenticate JFrog CLI with Artifactory.
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
@@ -20,52 +34,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
-if [ -z "${REPOSITORY_NPM}" ]; then
- echo "🤔 no npm repository is set, skipping npm configuration."
- echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
+if [ -z "${HAS_NPM}" ]; then
+ not_configured npm
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
cat << EOF > ~/.npmrc
-email=${ARTIFACTORY_EMAIL}
-registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
+${NPMRC}
EOF
- echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
+ config_complete
fi
# Configure the `pip` to use the Artifactory "python" repository.
-if [ -z "${REPOSITORY_PYPI}" ]; then
- echo "🤔 no pypi repository is set, skipping pip configuration."
- echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
+if [ -z "${HAS_PYPI}" ]; then
+ not_configured pypi
else
echo "🐍 Configuring pip..."
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
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
+${PIP_CONF}
EOF
+ config_complete
fi
# Configure Artifactory "go" repository.
-if [ -z "${REPOSITORY_GO}" ]; then
- echo "🤔 no go repository is set, skipping go configuration."
- echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
+if [ -z "${HAS_GO}" ]; then
+ not_configured go
else
echo "🐹 Configuring go..."
jf goc --global --repo-resolve "${REPOSITORY_GO}"
+ config_complete
fi
-echo "🥳 Configuration complete!"
# Configure the JFrog CLI to use the Artifactory "docker" repository.
-if [ -z "${REPOSITORY_DOCKER}" ]; then
- echo "🤔 no docker repository is set, skipping docker configuration."
- echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
+if [ -z "${HAS_DOCKER}" ]; then
+ not_configured docker
else
if command -v docker > /dev/null 2>&1; then
echo "🔑 Configuring 🐳 docker credentials..."
mkdir -p ~/.docker
- echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
+ ${REGISTER_DOCKER}
else
echo "🤔 no docker is installed, skipping docker configuration."
fi
@@ -96,20 +105,19 @@ echo "📦 Configuring JFrog CLI completion..."
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
# Generate the completion script
jf completion $SHELLNAME --install
+begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-token)"
# Add the completion script to the user's shell profile
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
- if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
- echo "" >> ~/.bashrc
- echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.bashrc
+ if ! grep -q "$begin_stanza" ~/.bashrc; then
+ printf "%s\n" "$begin_stanza" >> ~/.bashrc
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
echo "# END: jf CLI shell completion" >> ~/.bashrc
else
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
fi
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
- if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
- echo "" >> ~/.zshrc
- echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.zshrc
+ if ! grep -q "$begin_stanza" ~/.zshrc; then
+ printf "\n%s\n" "$begin_stanza" >> ~/.zshrc
echo "autoload -Uz compinit" >> ~/.zshrc
echo "compinit" >> ~/.zshrc
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
diff --git a/jupyter-notebook/README.md b/jupyter-notebook/README.md
index 6338f11..83d36cb 100644
--- a/jupyter-notebook/README.md
+++ b/jupyter-notebook/README.md
@@ -16,7 +16,7 @@ A module that adds Jupyter Notebook in your Coder template.
```tf
module "jupyter-notebook" {
source = "registry.coder.com/modules/jupyter-notebook/coder"
- version = "1.0.8"
+ version = "1.0.19"
agent_id = coder_agent.example.id
}
```
diff --git a/jupyter-notebook/run.sh b/jupyter-notebook/run.sh
index 4f8c4a2..0c7a9b8 100755
--- a/jupyter-notebook/run.sh
+++ b/jupyter-notebook/run.sh
@@ -7,14 +7,14 @@ printf "$${BOLD}Installing jupyter-notebook!\n"
# check if jupyter-notebook is installed
if ! command -v jupyter-notebook > /dev/null 2>&1; then
# install jupyter-notebook
- # check if python3 pip is installed
- if ! command -v pip3 > /dev/null 2>&1; then
- echo "pip3 is not installed"
- echo "Please install pip3 in your Dockerfile/VM image before running this script"
+ # check if pipx is installed
+ if ! command -v pipx > /dev/null 2>&1; then
+ echo "pipx is not installed"
+ echo "Please install pipx in your Dockerfile/VM image before using this module"
exit 1
fi
- # install jupyter-notebook
- pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyter
+ # install jupyter notebook
+ pipx install -q notebook
echo "🥳 jupyter-notebook has been installed\n\n"
else
echo "🥳 jupyter-notebook is already installed\n\n"
@@ -22,4 +22,4 @@ fi
echo "👷 Starting jupyter-notebook in background..."
echo "check logs at ${LOG_PATH}"
-$HOME/.local/bin/jupyter notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 &
+$HOME/.local/bin/jupyter-notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 &
diff --git a/jupyterlab/README.md b/jupyterlab/README.md
index 3d04cf3..52d5a50 100644
--- a/jupyterlab/README.md
+++ b/jupyterlab/README.md
@@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template.
```tf
module "jupyterlab" {
source = "registry.coder.com/modules/jupyterlab/coder"
- version = "1.0.8"
+ version = "1.0.22"
agent_id = coder_agent.example.id
}
```
diff --git a/jupyterlab/main.test.ts b/jupyterlab/main.test.ts
index 2597dc2..cf9ac1f 100644
--- a/jupyterlab/main.test.ts
+++ b/jupyterlab/main.test.ts
@@ -1,20 +1,20 @@
import { describe, expect, it } from "bun:test";
import {
+ execContainer,
executeScriptInContainer,
+ findResourceInstance,
+ runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
- findResourceInstance,
- runContainer,
- TerraformState,
- execContainer,
+ type TerraformState,
} from "../test";
// executes the coder script after installing pip
const executeScriptInContainerWithPip = async (
state: TerraformState,
image: string,
- shell: string = "sh",
+ shell = "sh",
): Promise<{
exitCode: number;
stdout: string[];
@@ -22,7 +22,7 @@ const executeScriptInContainerWithPip = async (
}> => {
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
- const respPip = await execContainer(id, [shell, "-c", "apk add py3-pip"]);
+ const respPipx = await execContainer(id, [shell, "-c", "apk add pipx"]);
const resp = await execContainer(id, [shell, "-c", instance.script]);
const stdout = resp.stdout.trim().split("\n");
const stderr = resp.stderr.trim().split("\n");
@@ -40,7 +40,7 @@ describe("jupyterlab", async () => {
agent_id: "foo",
});
- it("fails without pip3", async () => {
+ it("fails without pipx", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
@@ -48,14 +48,14 @@ describe("jupyterlab", async () => {
expect(output.exitCode).toBe(1);
expect(output.stdout).toEqual([
"\u001B[0;1mInstalling jupyterlab!",
- "pip3 is not installed",
- "Please install pip3 in your Dockerfile/VM image before running this script",
+ "pipx is not installed",
+ "Please install pipx in your Dockerfile/VM image before running this script",
]);
});
- // TODO: Add faster test to run with pip3.
+ // TODO: Add faster test to run with pipx.
// currently times out.
- // it("runs with pip3", async () => {
+ // it("runs with pipx", async () => {
// ...
// const output = await executeScriptInContainerWithPip(state, "alpine");
// ...
diff --git a/jupyterlab/main.tf b/jupyterlab/main.tf
index d7928f0..d66edb1 100644
--- a/jupyterlab/main.tf
+++ b/jupyterlab/main.tf
@@ -9,6 +9,9 @@ terraform {
}
}
+data "coder_workspace" "me" {}
+data "coder_workspace_owner" "me" {}
+
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
@@ -36,6 +39,12 @@ variable "share" {
}
}
+variable "subdomain" {
+ type = bool
+ description = "Determines whether JupyterLab will be accessed via its own subdomain or whether it will be accessed via a path on Coder."
+ default = true
+}
+
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
@@ -49,17 +58,18 @@ resource "coder_script" "jupyterlab" {
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
PORT : var.port
+ BASE_URL : var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
})
run_on_start = true
}
resource "coder_app" "jupyterlab" {
agent_id = var.agent_id
- slug = "jupyterlab"
+ slug = "jupyterlab" # sync with the usage in URL
display_name = "JupyterLab"
- url = "http://localhost:${var.port}"
+ url = var.subdomain ? "http://localhost:${var.port}" : "http://localhost:${var.port}/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
icon = "/icon/jupyter.svg"
- subdomain = true
+ subdomain = var.subdomain
share = var.share
order = var.order
}
diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh
index b040cec..aff21b7 100755
--- a/jupyterlab/run.sh
+++ b/jupyterlab/run.sh
@@ -1,5 +1,9 @@
#!/usr/bin/env sh
+if [ -n "${BASE_URL}" ]; then
+ BASE_URL_FLAG="--ServerApp.base_url=${BASE_URL}"
+fi
+
BOLD='\033[0;1m'
printf "$${BOLD}Installing jupyterlab!\n"
@@ -7,19 +11,25 @@ printf "$${BOLD}Installing jupyterlab!\n"
# check if jupyterlab is installed
if ! command -v jupyterlab > /dev/null 2>&1; then
# install jupyterlab
- # check if python3 pip is installed
- if ! command -v pip3 > /dev/null 2>&1; then
- echo "pip3 is not installed"
- echo "Please install pip3 in your Dockerfile/VM image before running this script"
+ # check if pipx is installed
+ if ! command -v pipx > /dev/null 2>&1; then
+ echo "pipx is not installed"
+ echo "Please install pipx in your Dockerfile/VM image before running this script"
exit 1
fi
# install jupyterlab
- pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyterlab
- echo "🥳 jupyterlab has been installed\n\n"
+ pipx install -q jupyterlab
+ printf "%s\n\n" "🥳 jupyterlab has been installed"
else
- echo "🥳 jupyterlab is already installed\n\n"
+ printf "%s\n\n" "🥳 jupyterlab is already installed"
fi
-echo "👷 Starting jupyterlab in background..."
-echo "check logs at ${LOG_PATH}"
-$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 &
+printf "👷 Starting jupyterlab in background..."
+printf "check logs at ${LOG_PATH}"
+$HOME/.local/bin/jupyter-lab --no-browser \
+ "$BASE_URL_FLAG" \
+ --ServerApp.ip='*' \
+ --ServerApp.port="${PORT}" \
+ --ServerApp.token='' \
+ --ServerApp.password='' \
+ > "${LOG_PATH}" 2>&1 &
diff --git a/kasmvnc/README.md b/kasmvnc/README.md
new file mode 100644
index 0000000..3b7fe50
--- /dev/null
+++ b/kasmvnc/README.md
@@ -0,0 +1,23 @@
+---
+display_name: KasmVNC
+description: A modern open source VNC server
+icon: ../.icons/kasmvnc.svg
+maintainer_github: coder
+verified: true
+tags: [helper, vnc, desktop]
+---
+
+# KasmVNC
+
+Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and create an app to access it via the dashboard.
+
+```tf
+module "kasmvnc" {
+ source = "registry.coder.com/modules/kasmvnc/coder"
+ version = "1.0.22"
+ agent_id = coder_agent.example.id
+ desktop_environment = "xfce"
+}
+```
+
+> **Note:** This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use `codercom/enterprise-desktop` image.
diff --git a/kasmvnc/main.test.ts b/kasmvnc/main.test.ts
new file mode 100644
index 0000000..0116d05
--- /dev/null
+++ b/kasmvnc/main.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "bun:test";
+import {
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+} from "../test";
+
+const allowedDesktopEnvs = ["xfce", "kde", "gnome", "lxde", "lxqt"] as const;
+type AllowedDesktopEnv = (typeof allowedDesktopEnvs)[number];
+
+type TestVariables = Readonly<{
+ agent_id: string;
+ desktop_environment: AllowedDesktopEnv;
+ port?: string;
+ kasm_version?: string;
+}>;
+
+describe("Kasm VNC", async () => {
+ await runTerraformInit(import.meta.dir);
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "foo",
+ desktop_environment: "gnome",
+ });
+
+ it("Successfully installs for all expected Kasm desktop versions", async () => {
+ for (const v of allowedDesktopEnvs) {
+ const applyWithEnv = () => {
+ runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ desktop_environment: v,
+ });
+ };
+
+ expect(applyWithEnv).not.toThrow();
+ }
+ });
+});
diff --git a/kasmvnc/main.tf b/kasmvnc/main.tf
new file mode 100644
index 0000000..3a730ff
--- /dev/null
+++ b/kasmvnc/main.tf
@@ -0,0 +1,63 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 0.12"
+ }
+ }
+}
+
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "port" {
+ type = number
+ description = "The port to run KasmVNC on."
+ default = 6800
+}
+
+variable "kasm_version" {
+ type = string
+ description = "Version of KasmVNC to install."
+ default = "1.3.2"
+}
+
+variable "desktop_environment" {
+ type = string
+ description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
+ validation {
+ condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
+ error_message = "Invalid desktop environment. Please specify a valid desktop environment."
+ }
+}
+
+resource "coder_script" "kasm_vnc" {
+ agent_id = var.agent_id
+ display_name = "KasmVNC"
+ icon = "/icon/kasmvnc.svg"
+ script = templatefile("${path.module}/run.sh", {
+ PORT : var.port,
+ DESKTOP_ENVIRONMENT : var.desktop_environment,
+ VERSION : var.kasm_version
+ })
+ run_on_start = true
+}
+
+resource "coder_app" "kasm_vnc" {
+ agent_id = var.agent_id
+ slug = "kasm-vnc"
+ display_name = "kasmVNC"
+ url = "http://localhost:${var.port}"
+ icon = "/icon/kasmvnc.svg"
+ subdomain = true
+ share = "owner"
+ healthcheck {
+ url = "http://localhost:${var.port}/app"
+ interval = 5
+ threshold = 5
+ }
+}
diff --git a/kasmvnc/run.sh b/kasmvnc/run.sh
new file mode 100644
index 0000000..b831537
--- /dev/null
+++ b/kasmvnc/run.sh
@@ -0,0 +1,179 @@
+#!/usr/bin/env bash
+
+#!/bin/bash
+
+# Function to check if vncserver is already installed
+check_installed() {
+ if command -v vncserver &> /dev/null; then
+ echo "vncserver is already installed."
+ return 0 # Don't exit, just indicate it's installed
+ else
+ return 1 # Indicates not installed
+ fi
+}
+
+# Function to download a file using wget, curl, or busybox as a fallback
+download_file() {
+ local url=$1
+ local output=$2
+ if command -v wget &> /dev/null; then
+ wget $url -O $output
+ elif command -v curl &> /dev/null; then
+ curl -fsSL $url -o $output
+ elif command -v busybox &> /dev/null; then
+ busybox wget -O $output $url
+ else
+ echo "Neither wget, curl, nor busybox is installed. Please install one of them to proceed."
+ exit 1
+ fi
+}
+
+# Function to install kasmvncserver for debian-based distros
+install_deb() {
+ local url=$1
+ download_file $url /tmp/kasmvncserver.deb
+ sudo apt-get update
+ DEBIAN_FRONTEND=noninteractive sudo apt-get install --yes -qq --no-install-recommends --no-install-suggests /tmp/kasmvncserver.deb
+ sudo adduser $USER ssl-cert
+ rm /tmp/kasmvncserver.deb
+}
+
+# Function to install kasmvncserver for Oracle 8
+install_rpm_oracle8() {
+ local url=$1
+ download_file $url /tmp/kasmvncserver.rpm
+ sudo dnf config-manager --set-enabled ol8_codeready_builder
+ sudo dnf install oracle-epel-release-el8 -y
+ sudo dnf localinstall /tmp/kasmvncserver.rpm -y
+ sudo usermod -aG kasmvnc-cert $USER
+ rm /tmp/kasmvncserver.rpm
+}
+
+# Function to install kasmvncserver for CentOS 7
+install_rpm_centos7() {
+ local url=$1
+ download_file $url /tmp/kasmvncserver.rpm
+ sudo yum install epel-release -y
+ sudo yum install /tmp/kasmvncserver.rpm -y
+ sudo usermod -aG kasmvnc-cert $USER
+ rm /tmp/kasmvncserver.rpm
+}
+
+# Function to install kasmvncserver for rpm-based distros
+install_rpm() {
+ local url=$1
+ download_file $url /tmp/kasmvncserver.rpm
+ sudo rpm -i /tmp/kasmvncserver.rpm
+ rm /tmp/kasmvncserver.rpm
+}
+
+# Function to install kasmvncserver for Alpine Linux
+install_alpine() {
+ local url=$1
+ download_file $url /tmp/kasmvncserver.tgz
+ tar -xzf /tmp/kasmvncserver.tgz -C /usr/local/bin/
+ rm /tmp/kasmvncserver.tgz
+}
+
+# Detect system information
+distro=$(grep "^ID=" /etc/os-release | awk -F= '{print $2}')
+version=$(grep "^VERSION_ID=" /etc/os-release | awk -F= '{print $2}' | tr -d '"')
+arch=$(uname -m)
+
+echo "Detected Distribution: $distro"
+echo "Detected Version: $version"
+echo "Detected Architecture: $arch"
+
+# Map arch to package arch
+if [[ "$arch" == "x86_64" ]]; then
+ if [[ "$distro" == "ubuntu" || "$distro" == "debian" || "$distro" == "kali" ]]; then
+ arch="amd64"
+ else
+ arch="x86_64"
+ fi
+elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
+ if [[ "$distro" == "ubuntu" || "$distro" == "debian" || "$distro" == "kali" ]]; then
+ arch="arm64"
+ else
+ arch="aarch64"
+ fi
+else
+ echo "Unsupported architecture: $arch"
+ exit 1
+fi
+
+# Check if vncserver is installed, and install if not
+if ! check_installed; then
+ echo "Installing KASM version: ${VERSION}"
+ case $distro in
+ ubuntu | debian | kali)
+ case $version in
+ "20.04")
+ install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_focal_${VERSION}_$${arch}.deb"
+ ;;
+ "22.04")
+ install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_jammy_${VERSION}_$${arch}.deb"
+ ;;
+ "24.04")
+ install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_noble_${VERSION}_$${arch}.deb"
+ ;;
+ *)
+ echo "Unsupported Ubuntu/Debian/Kali version: $${version}"
+ exit 1
+ ;;
+ esac
+ ;;
+ oracle)
+ if [[ "$version" == "8" ]]; then
+ install_rpm_oracle8 "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_oracle_8_${VERSION}_$${arch}.rpm"
+ else
+ echo "Unsupported Oracle version: $${version}"
+ exit 1
+ fi
+ ;;
+ centos)
+ if [[ "$version" == "7" ]]; then
+ install_rpm_centos7 "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_centos_core_${VERSION}_$${arch}.rpm"
+ else
+ install_rpm "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_centos_core_${VERSION}_$${arch}.rpm"
+ fi
+ ;;
+ alpine)
+ if [[ "$version" == "3.17" || "$version" == "3.18" || "$version" == "3.19" || "$version" == "3.20" ]]; then
+ install_alpine "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvnc.alpine_$${version}_$${arch}.tgz"
+ else
+ echo "Unsupported Alpine version: $${version}"
+ exit 1
+ fi
+ ;;
+ fedora | opensuse)
+ install_rpm "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_$${distro}_$${version}_${VERSION}_$${arch}.rpm"
+ ;;
+ *)
+ echo "Unsupported distribution: $${distro}"
+ exit 1
+ ;;
+ esac
+else
+ echo "vncserver already installed. Skipping installation."
+fi
+
+# Coder port-forwarding from dashboard only supports HTTP
+sudo bash -c "cat > /etc/kasmvnc/kasmvnc.yaml < /tmp/kasmvncserver.log 2>&1 &
diff --git a/nodejs/main.test.ts b/nodejs/main.test.ts
index 07fc7a5..39e48f4 100644
--- a/nodejs/main.test.ts
+++ b/nodejs/main.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, it } from "bun:test";
+import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
describe("nodejs", async () => {
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 1010942..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,264 +0,0 @@
-{
- "name": "modules",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "modules",
- "devDependencies": {
- "bun-types": "^1.0.18",
- "gray-matter": "^4.0.3",
- "marked": "^12.0.0",
- "prettier": "^3.2.5",
- "prettier-plugin-sh": "^0.13.1",
- "prettier-plugin-terraform-formatter": "^1.2.1"
- },
- "peerDependencies": {
- "typescript": "^5.3.3"
- }
- },
- "node_modules/@types/node": {
- "version": "20.12.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
- "integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
- "dev": true,
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/@types/ws": {
- "version": "8.5.10",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
- "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
- "dev": true,
- "dependencies": {
- "@types/node": "*"
- }
- },
- "node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
- "node_modules/bun-types": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz",
- "integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==",
- "dev": true,
- "dependencies": {
- "@types/node": "~20.12.8",
- "@types/ws": "~8.5.10"
- }
- },
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
- "dev": true,
- "dependencies": {
- "is-extendable": "^0.1.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/gray-matter": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
- "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
- "dev": true,
- "dependencies": {
- "js-yaml": "^3.13.1",
- "kind-of": "^6.0.2",
- "section-matter": "^1.0.0",
- "strip-bom-string": "^1.0.0"
- },
- "engines": {
- "node": ">=6.0"
- }
- },
- "node_modules/is-extendable": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
- "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
- "dev": true,
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/marked": {
- "version": "12.0.2",
- "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
- "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
- "dev": true,
- "bin": {
- "marked": "bin/marked.js"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/mvdan-sh": {
- "version": "0.10.1",
- "resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
- "integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
- "dev": true
- },
- "node_modules/prettier": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
- "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
- "dev": true,
- "peer": true,
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
- }
- },
- "node_modules/prettier-plugin-sh": {
- "version": "0.13.1",
- "resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
- "integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
- "dev": true,
- "dependencies": {
- "mvdan-sh": "^0.10.1",
- "sh-syntax": "^0.4.1"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/unts"
- },
- "peerDependencies": {
- "prettier": "^3.0.0"
- }
- },
- "node_modules/prettier-plugin-terraform-formatter": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
- "integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
- "dev": true,
- "peerDependencies": {
- "prettier": ">= 1.16.0"
- },
- "peerDependenciesMeta": {
- "prettier": {
- "optional": true
- }
- }
- },
- "node_modules/section-matter": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
- "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
- "dev": true,
- "dependencies": {
- "extend-shallow": "^2.0.1",
- "kind-of": "^6.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/sh-syntax": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
- "integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
- "dev": true,
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/unts"
- }
- },
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true
- },
- "node_modules/strip-bom-string": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
- "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
- "dev": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/tslib": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
- "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
- "dev": true
- },
- "node_modules/typescript": {
- "version": "5.5.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
- "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
- "peer": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "dev": true
- }
- }
-}
diff --git a/package.json b/package.json
index f3136b1..eea421d 100644
--- a/package.json
+++ b/package.json
@@ -8,15 +8,15 @@
"update-version": "./update-version.sh"
},
"devDependencies": {
- "bun-types": "^1.0.18",
+ "bun-types": "^1.1.23",
"gray-matter": "^4.0.3",
- "marked": "^12.0.0",
- "prettier": "^3.2.5",
+ "marked": "^12.0.2",
+ "prettier": "^3.3.3",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
- "typescript": "^5.3.3"
+ "typescript": "^5.5.4"
},
"prettier": {
"plugins": [
diff --git a/personalize/main.test.ts b/personalize/main.test.ts
index 9c8134e..b499a0b 100644
--- a/personalize/main.test.ts
+++ b/personalize/main.test.ts
@@ -1,13 +1,9 @@
-import { readableStreamToText, spawn } from "bun";
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
- runContainer,
- execContainer,
- findResourceInstance,
} from "../test";
describe("personalize", async () => {
diff --git a/slackme/main.test.ts b/slackme/main.test.ts
index eca4f5d..d8d0624 100644
--- a/slackme/main.test.ts
+++ b/slackme/main.test.ts
@@ -72,7 +72,7 @@ executed`,
it("formats execution with milliseconds", async () => {
await assertSlackMessage({
command: "echo test",
- format: `$COMMAND took $DURATION`,
+ format: "$COMMAND took $DURATION",
durationMS: 150,
output: "echo test took 150ms",
});
@@ -81,7 +81,7 @@ executed`,
it("formats execution with seconds", async () => {
await assertSlackMessage({
command: "echo test",
- format: `$COMMAND took $DURATION`,
+ format: "$COMMAND took $DURATION",
durationMS: 15000,
output: "echo test took 15.0s",
});
@@ -90,7 +90,7 @@ executed`,
it("formats execution with minutes", async () => {
await assertSlackMessage({
command: "echo test",
- format: `$COMMAND took $DURATION`,
+ format: "$COMMAND took $DURATION",
durationMS: 120000,
output: "echo test took 2m 0.0s",
});
@@ -99,7 +99,7 @@ executed`,
it("formats execution with hours", async () => {
await assertSlackMessage({
command: "echo test",
- format: `$COMMAND took $DURATION`,
+ format: "$COMMAND took $DURATION",
durationMS: 60000 * 60,
output: "echo test took 1hr 0m 0.0s",
});
diff --git a/terraform_validate.sh b/terraform_validate.sh
index 292c94c..492e65a 100755
--- a/terraform_validate.sh
+++ b/terraform_validate.sh
@@ -4,25 +4,25 @@ 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
+ 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 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)
+ # 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
+ for dir in $subdirs; do
+ run_terraform "$dir"
+ done
}
# Run the main script
diff --git a/test.ts b/test.ts
index 6bdf9d9..5437374 100644
--- a/test.ts
+++ b/test.ts
@@ -1,6 +1,6 @@
import { readableStreamToText, spawn } from "bun";
-import { afterEach, expect, it } from "bun:test";
-import { readFile, unlink } from "fs/promises";
+import { expect, it } from "bun:test";
+import { readFile, unlink } from "node:fs/promises";
export const runContainer = async (
image: string,
@@ -21,7 +21,8 @@ export const runContainer = async (
"-c",
init,
]);
- let containerID = await readableStreamToText(proc.stdout);
+
+ const containerID = await readableStreamToText(proc.stdout);
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(containerID);
@@ -36,7 +37,7 @@ export const runContainer = async (
export const executeScriptInContainer = async (
state: TerraformState,
image: string,
- shell: string = "sh",
+ shell = "sh",
): Promise<{
exitCode: number;
stdout: string[];
@@ -108,12 +109,17 @@ export interface TerraformState {
resources: [TerraformStateResource, ...TerraformStateResource[]];
}
+type TerraformVariables = Record;
+
export interface CoderScriptAttributes {
script: string;
agent_id: string;
url: string;
}
+export type ResourceInstance =
+ T extends "coder_script" ? CoderScriptAttributes : Record;
+
/**
* finds the first instance of the given resource type in the given state. If
* name is specified, it will only find the instance with the given name.
@@ -122,10 +128,7 @@ export const findResourceInstance = (
state: TerraformState,
type: T,
name?: string,
- // if type is "coder_script" return CoderScriptAttributes
-): T extends "coder_script"
- ? CoderScriptAttributes
- : Record => {
+): ResourceInstance => {
const resource = state.resources.find(
(resource) =>
resource.type === type && (name ? resource.name === name : true),
@@ -138,16 +141,17 @@ export const findResourceInstance = (
`Resource ${type} has ${resource.instances.length} instances`,
);
}
- return resource.instances[0].attributes as any;
+
+ return resource.instances[0].attributes as ResourceInstance;
};
/**
* Creates a test-case for each variable provided and ensures that the apply
* fails without it.
*/
-export const testRequiredVariables = >(
+export const testRequiredVariables = (
dir: string,
- vars: TVars,
+ vars: Readonly,
) => {
// Ensures that all required variables are provided.
it("required variables", async () => {
@@ -155,15 +159,15 @@ export const testRequiredVariables = >(
});
const varNames = Object.keys(vars);
- varNames.forEach((varName) => {
+ for (const varName of varNames) {
// Ensures that every variable provided is required!
- it("missing variable " + varName, async () => {
- const localVars: Record = {};
- varNames.forEach((otherVarName) => {
+ it(`missing variable: ${varName}`, async () => {
+ const localVars: TerraformVariables = {};
+ for (const otherVarName of varNames) {
if (otherVarName !== varName) {
localVars[otherVarName] = vars[otherVarName];
}
- });
+ }
try {
await runTerraformApply(dir, localVars);
@@ -179,7 +183,7 @@ export const testRequiredVariables = >(
}
throw new Error(`${varName} is not a required variable!`);
});
- });
+ }
};
/**
@@ -187,11 +191,9 @@ export const testRequiredVariables = >(
* fine to run in parallel with other instances of this function, as it uses a
* random state file.
*/
-export const runTerraformApply = async <
- TVars extends Readonly>,
->(
+export const runTerraformApply = async (
dir: string,
- vars: TVars,
+ vars: Readonly,
env?: Record,
): Promise => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
diff --git a/tsconfig.json b/tsconfig.json
index dd38e58..c7a5d26 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,14 @@
{
"compilerOptions": {
- "target": "esnext",
- "module": "esnext",
+ // If we were just compiling for the tests, we could safely target ESNext at
+ // all times, but just because we've been starting to add more runtime logic
+ // files to some of the modules, erring on the side of caution by having a
+ // older compilation target
+ "target": "ES6",
+ "module": "ESNext",
"strict": true,
"allowSyntheticDefaultImports": true,
- "moduleResolution": "nodenext",
+ "moduleResolution": "node",
"types": ["bun-types"]
}
}
diff --git a/update-version.sh b/update-version.sh
index 5deb63b..b062736 100755
--- a/update-version.sh
+++ b/update-version.sh
@@ -1,20 +1,24 @@
#!/usr/bin/env bash
-# This script updates the version number in the README.md files of all modules
-# to the latest tag in the repository. It is intended to be run from the root
+# This script increments the version number in the README.md files of all modules
+# by 1 patch version. It is intended to be run from the root
# of the repository or by using the `bun update-version` command.
set -euo pipefail
current_tag=$(git describe --tags --abbrev=0)
-previous_tag=$(git describe --tags --abbrev=0 $current_tag^)
-mapfile -t changed_dirs < <(git diff --name-only "$previous_tag"..."$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u)
-LATEST_TAG=$(git describe --abbrev=0 --tags | sed 's/^v//') || exit $?
+# Increment the patch version
+LATEST_TAG=$(echo "$current_tag" | sed 's/^v//' | awk -F. '{print $1"."$2"."$3+1}') || exit $?
+# List directories with changes that are not README.md or test files
+mapfile -t changed_dirs < <(git diff --name-only "$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u)
+
+echo "Directories with changes: ${changed_dirs[*]}"
+
+# Iterate over directories and update version in README.md
for dir in "${changed_dirs[@]}"; do
if [[ -f "$dir/README.md" ]]; then
- echo "Bumping version in $dir/README.md"
file="$dir/README.md"
tmpfile=$(mktemp /tmp/tempfile.XXXXXX)
awk -v tag="$LATEST_TAG" '{
@@ -25,5 +29,12 @@ for dir in "${changed_dirs[@]}"; do
print
}
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
+
+ # Check if the README.md file has changed
+ if ! git diff --quiet -- "$dir/README.md"; then
+ echo "Bumping version in $dir/README.md from $current_tag to $LATEST_TAG (incremented)"
+ else
+ echo "Version in $dir/README.md is already up to date"
+ fi
fi
done
diff --git a/vault-jwt/README.md b/vault-jwt/README.md
new file mode 100644
index 0000000..939bed2
--- /dev/null
+++ b/vault-jwt/README.md
@@ -0,0 +1,77 @@
+---
+display_name: Hashicorp Vault Integration (JWT)
+description: Authenticates with Vault using a JWT from Coder's OIDC provider
+icon: ../.icons/vault.svg
+maintainer_github: coder
+partner_github: hashicorp
+verified: true
+tags: [helper, integration, vault, jwt, oidc]
+---
+
+# Hashicorp Vault Integration (JWT)
+
+This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/auth#openid-connect) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
+
+```tf
+module "vault" {
+ source = "registry.coder.com/modules/vault-jwt/coder"
+ version = "1.0.20"
+ agent_id = coder_agent.example.id
+ vault_addr = "https://vault.example.com"
+ vault_jwt_role = "coder" # The Vault role to use for authentication
+}
+```
+
+Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
+
+```shell
+vault kv get -namespace=coder -mount=secrets coder
+```
+
+or using the Vault API:
+
+```shell
+curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
+```
+
+## Examples
+
+### Configure Vault integration with a non standard auth path (default is "jwt")
+
+```tf
+module "vault" {
+ source = "registry.coder.com/modules/vault-jwt/coder"
+ version = "1.0.20"
+ agent_id = coder_agent.example.id
+ vault_addr = "https://vault.example.com"
+ vault_jwt_auth_path = "oidc"
+ vault_jwt_role = "coder" # The Vault role to use for authentication
+}
+```
+
+### Map workspace owner's group to a Vault role
+
+```tf
+data "coder_workspace_owner" "me" {}
+
+module "vault" {
+ source = "registry.coder.com/modules/vault-jwt/coder"
+ version = "1.0.20"
+ agent_id = coder_agent.example.id
+ vault_addr = "https://vault.example.com"
+ vault_jwt_role = data.coder_workspace_owner.me.groups[0]
+}
+```
+
+### Install a specific version of the Vault CLI
+
+```tf
+module "vault" {
+ source = "registry.coder.com/modules/vault-jwt/coder"
+ version = "1.0.20"
+ agent_id = coder_agent.example.id
+ vault_addr = "https://vault.example.com"
+ vault_jwt_role = "coder" # The Vault role to use for authentication
+ vault_cli_version = "1.17.5"
+}
+```
diff --git a/vault-jwt/main.test.ts b/vault-jwt/main.test.ts
new file mode 100644
index 0000000..2fda3d7
--- /dev/null
+++ b/vault-jwt/main.test.ts
@@ -0,0 +1,12 @@
+import { describe } from "bun:test";
+import { runTerraformInit, testRequiredVariables } from "../test";
+
+describe("vault-jwt", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "foo",
+ vault_addr: "foo",
+ vault_jwt_role: "foo",
+ });
+});
diff --git a/vault-jwt/main.tf b/vault-jwt/main.tf
new file mode 100644
index 0000000..adcc34d
--- /dev/null
+++ b/vault-jwt/main.tf
@@ -0,0 +1,64 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 0.12.4"
+ }
+ }
+}
+
+# Add required variables for your modules and remove any unneeded variables
+variable "agent_id" {
+ type = string
+ description = "The ID of a Coder agent."
+}
+
+variable "vault_addr" {
+ type = string
+ description = "The address of the Vault server."
+}
+
+variable "vault_jwt_auth_path" {
+ type = string
+ description = "The path to the Vault JWT auth method."
+ default = "jwt"
+}
+
+variable "vault_jwt_role" {
+ type = string
+ description = "The name of the Vault role to use for authentication."
+}
+
+variable "vault_cli_version" {
+ type = string
+ description = "The version of Vault to install."
+ default = "latest"
+ validation {
+ condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.vault_cli_version))
+ error_message = "Vault version must be in the format 0.0.0 or latest"
+ }
+}
+
+resource "coder_script" "vault" {
+ agent_id = var.agent_id
+ display_name = "Vault (GitHub)"
+ icon = "/icon/vault.svg"
+ script = templatefile("${path.module}/run.sh", {
+ CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token,
+ VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path,
+ VAULT_JWT_ROLE : var.vault_jwt_role,
+ VAULT_CLI_VERSION : var.vault_cli_version,
+ })
+ run_on_start = true
+ start_blocks_login = true
+}
+
+resource "coder_env" "vault_addr" {
+ agent_id = var.agent_id
+ name = "VAULT_ADDR"
+ value = var.vault_addr
+}
+
+data "coder_workspace_owner" "me" {}
diff --git a/vault-jwt/run.sh b/vault-jwt/run.sh
new file mode 100644
index 0000000..ef45884
--- /dev/null
+++ b/vault-jwt/run.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+
+# Convert all templated variables to shell variables
+VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
+VAULT_JWT_AUTH_PATH=${VAULT_JWT_AUTH_PATH}
+VAULT_JWT_ROLE=${VAULT_JWT_ROLE}
+CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN}
+
+fetch() {
+ dest="$1"
+ url="$2"
+ if command -v curl > /dev/null 2>&1; then
+ curl -sSL --fail "$${url}" -o "$${dest}"
+ elif command -v wget > /dev/null 2>&1; then
+ wget -O "$${dest}" "$${url}"
+ elif command -v busybox > /dev/null 2>&1; then
+ busybox wget -O "$${dest}" "$${url}"
+ else
+ printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
+ exit 1
+ fi
+}
+
+unzip_safe() {
+ if command -v unzip > /dev/null 2>&1; then
+ command unzip "$@"
+ elif command -v busybox > /dev/null 2>&1; then
+ busybox unzip "$@"
+ else
+ printf "unzip or busybox is not installed. Please install unzip in your image.\n"
+ exit 1
+ fi
+}
+
+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 VAULT_CLI_VERSION is 'latest'
+ if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
+ 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"
+ return 1
+ fi
+ VAULT_CLI_VERSION=$${LATEST_VERSION}
+ fi
+
+ # Check if the vault CLI is installed and has the correct version
+ installation_needed=1
+ if command -v vault > /dev/null 2>&1; then
+ CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
+ if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
+ printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
+ installation_needed=0
+ fi
+ fi
+
+ if [ $${installation_needed} -eq 1 ]; then
+ # Download and install Vault
+ if [ -z "$${CURRENT_VERSION}" ]; then
+ printf "Installing Vault CLI ...\n\n"
+ else
+ printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${VAULT_CLI_VERSION}"
+ fi
+ fetch vault.zip "https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_linux_$${ARCH}.zip"
+ if [ $? -ne 0 ]; then
+ printf "Failed to download Vault.\n"
+ return 1
+ fi
+ if ! unzip_safe vault.zip; then
+ printf "Failed to unzip Vault.\n"
+ return 1
+ fi
+ rm vault.zip
+ if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
+ printf "Vault installed successfully!\n\n"
+ else
+ mkdir -p ~/.local/bin
+ if ! mv vault ~/.local/bin/vault; then
+ printf "Failed to move Vault to local bin.\n"
+ return 1
+ fi
+ printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
+ fi
+ fi
+ return 0
+}
+
+TMP=$(mktemp -d)
+if ! (
+ cd "$TMP"
+ install
+); then
+ echo "Failed to install Vault CLI."
+ exit 1
+fi
+rm -rf "$TMP"
+
+# Authenticate with Vault
+printf "🔑 Authenticating with Vault ...\n\n"
+echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=-
+printf "🥳 Vault authentication complete!\n\n"
+printf "You can now use Vault CLI to access secrets.\n"
diff --git a/vscode-desktop/main.test.ts b/vscode-desktop/main.test.ts
index 207b492..7aa144e 100644
--- a/vscode-desktop/main.test.ts
+++ b/vscode-desktop/main.test.ts
@@ -22,7 +22,7 @@ describe("vscode-desktop", async () => {
);
const coder_app = state.resources.find(
- (res) => res.type == "coder_app" && res.name == "vscode",
+ (res) => res.type === "coder_app" && res.name === "vscode",
);
expect(coder_app).not.toBeNull();
@@ -79,7 +79,7 @@ describe("vscode-desktop", async () => {
});
const coder_app = state.resources.find(
- (res) => res.type == "coder_app" && res.name == "vscode",
+ (res) => res.type === "coder_app" && res.name === "vscode",
);
expect(coder_app).not.toBeNull();
diff --git a/vscode-web/README.md b/vscode-web/README.md
index ba395d0..821d518 100644
--- a/vscode-web/README.md
+++ b/vscode-web/README.md
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
- version = "1.0.14"
+ version = "1.0.20"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -29,7 +29,7 @@ module "vscode-web" {
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
- version = "1.0.14"
+ version = "1.0.20"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -42,7 +42,7 @@ module "vscode-web" {
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
- version = "1.0.14"
+ version = "1.0.20"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -56,7 +56,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
- version = "1.0.14"
+ version = "1.0.20"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
diff --git a/vscode-web/run.sh b/vscode-web/run.sh
index 651e126..1738364 100755
--- a/vscode-web/run.sh
+++ b/vscode-web/run.sh
@@ -78,27 +78,25 @@ for extension in "$${EXTENSIONLIST[@]}"; do
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output"
- exit 1
fi
done
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
- exit 0
- fi
-
- WORKSPACE_DIR="$HOME"
- if [ -n "${FOLDER}" ]; then
- WORKSPACE_DIR="${FOLDER}"
- fi
-
- if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
- printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
- extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
- for extension in $extensions; do
- $VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
- done
+ else
+ WORKSPACE_DIR="$HOME"
+ if [ -n "${FOLDER}" ]; then
+ WORKSPACE_DIR="${FOLDER}"
+ fi
+
+ if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
+ printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
+ extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
+ for extension in $extensions; do
+ $VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
+ done
+ fi
fi
fi
diff --git a/windows-rdp/README.md b/windows-rdp/README.md
index e8c5a1c..c4d35fd 100644
--- a/windows-rdp/README.md
+++ b/windows-rdp/README.md
@@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
# AWS example. See below for examples of using this module with other providers
module "windows_rdp" {
source = "registry.coder.com/modules/windows-rdp/coder"
- version = "1.0.16"
+ version = "1.0.18"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
@@ -33,7 +33,7 @@ module "windows_rdp" {
```tf
module "windows_rdp" {
source = "registry.coder.com/modules/windows-rdp/coder"
- version = "1.0.16"
+ version = "1.0.18"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
@@ -45,7 +45,7 @@ module "windows_rdp" {
```tf
module "windows_rdp" {
source = "registry.coder.com/modules/windows-rdp/coder"
- version = "1.0.16"
+ version = "1.0.18"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
diff --git a/windows-rdp/main.test.ts b/windows-rdp/main.test.ts
index 61075d9..ba5e21a 100644
--- a/windows-rdp/main.test.ts
+++ b/windows-rdp/main.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test";
import {
- TerraformState,
+ type TerraformState,
runTerraformApply,
runTerraformInit,
testRequiredVariables,