Compare commits

...

32 Commits

Author SHA1 Message Date
nathan 63e23dd824 Update 'kasmvnc/run.sh' 4 months ago
M Atif Ali 4d2531548f
Revert "feat(vscode-web): allow pinning vscode-web binary to a specific commit ID" (#403) 4 months ago
Roger Chao 3d33656bcc
feat(vscode-web): allow pinning vscode-web binary to a specific commit ID (#402)
Adds support for specifying a commit ID to pin the vscode-web binary
version in the module.
4 months ago
Guspan Tanadi c390ed005f
docs: update section links JSON Settings (#401) 5 months ago
M Atif Ali d78925d05f
fix: handle extensions.json comments in vscode-web and code-server (#398) 5 months ago
dstoffel 22b2ad5fec
fix(filebrowser): failed to set server_base_path if db is not existing (#393)
Co-authored-by: M Atif Ali <me@matifali.dev>
5 months ago
Charles Augello 6e66ff59a3
fix(dotfiles): handle failures in dotfiles installation (#387) 5 months ago
Edward Angert 19cdb78319
docs: add jetbrains min requirements note and link (#397) 5 months ago
dependabot[bot] 4a93bf11e7
chore(deps): bump google-github-actions/auth from 2.1.7 to 2.1.8 (#395)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5 months ago
dependabot[bot] 46a4113e51
chore(deps): bump google-github-actions/setup-gcloud from 2.1.2 to 2.1.4 (#396)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5 months ago
Guspan Tanadi 7c8aa504ae
docs(vault-jwt): update OIDC docs link (#388)
Co-authored-by: M Atif Ali <atif@coder.com>
5 months ago
Michael Smith e64f1ede52
fix: ensure Terraform is available for integration tests (#390) 5 months ago
Phorcys c8a42f6202
chore: make `agent_name` unused (#383) 6 months ago
Muhammad Atif Ali e9238f107a
feat(jetbrains-gateway): add RustRover to JetBrains Gateway module (#382) 6 months ago
Phorcys e94f70a286
chore(git-commit-signing): add version warning in README (#377) 6 months ago
Muhammad Atif Ali 7654140330
docs: promote count usage to prevent module download on stop (#371) 6 months ago
Cian Johnston bc6490f0d3
ci: disable vercel redeploys (#376) 6 months ago
Benjamin Peinhardt 482ed84399
feat: ci to build new registry on push to main (#363)
This PR adds a github actions workflow for deploying new revisions of
the registry on pushes to main.
This means updating the new registry will continue to be as simple as
landing a PR in this repo, but it should only take as long as the docker
container takes to build to see the updates live :)

For now, updates go to dev for manual inspection, but once we're confident in the build process they'll go straight to main as well.
6 months ago
Phorcys 32b69016a0
fix(vscode-web): set `settings` variable type to `any` (#369) 6 months ago
Phorcys 6d2739131a
fix(code-server): set `settings` variable type to `any` (#368) 6 months ago
Muhammad Atif Ali cbd06b1135
Improve incident management in Instatus check script (#346) 7 months ago
Muhammad Atif Ali 675c82367a
feat(jetbrains-gateway): bump to 2024.3 (#355) 7 months ago
Mathias Fredriksson bf697e1fa4
fix(update-versions.sh): handle markdown/tf block nesting when updating version (#356) 7 months ago
Muhammad Atif Ali b345e62ac1
feat: add Amazon DCV Windows module (#345) 7 months ago
Michael Smith 6597a2d547
chore: add updates to force redeployment on Vercel (#348)
## Changes made
- Updated `check.sh` script to add support for automatic re-deploying in
the event that the the registry has a partial/full outage.

---------

Co-authored-by: Cian Johnston <cian@coder.com>
7 months ago
Muhammad Atif Ali 5101c27c83
chore: integrate Instatus in check script (#342) 7 months ago
Muhammad Atif Ali 90bfbfdc40
chore: add health check badge (#341) 7 months ago
Cian Johnston 57d96ca27f
ci: add script to check modules on registry.coder.com (#340)
Added a script + corresponding GitHub action to check active modules on registry.coder.com
7 months ago
Tao Chen f5ab7995d1
feat(filebrowser): check if already installed (#334)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
8 months ago
djarbz 528a8a9fea
fix(kasmvnc): optimize KasmVNC deployment script (#329)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
8 months ago
Kerwin Bryant 87854707bc
feat(jetbrains-gateway): add releases_base_link/download_base_link variables (#333) 8 months ago
Roger Chao b53554b4e4
fix(jupyterlab): update command -v from jupyterlab to jupyter-lab (#328)
Update `command -v` from `jupyterlab` to `jupyter-lab` to check to if
jupyterlab binary is installed.
8 months ago

@ -0,0 +1,203 @@
#!/usr/bin/env bash
set -o pipefail
set -u
VERBOSE="${VERBOSE:-0}"
if [[ "${VERBOSE}" -ne "0" ]]; then
set -x
fi
# List of required environment variables
required_vars=(
"INSTATUS_API_KEY"
"INSTATUS_PAGE_ID"
"INSTATUS_COMPONENT_ID"
"VERCEL_API_KEY"
)
# Check if each required variable is set
for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "Error: Environment variable '$var' is not set."
exit 1
fi
done
REGISTRY_BASE_URL="${REGISTRY_BASE_URL:-https://registry.coder.com}"
status=0
declare -a modules=()
declare -a failures=()
# Collect all module directories containing a main.tf file
for path in $(find . -maxdepth 2 -not -path '*/.*' -type f -name main.tf | cut -d '/' -f 2 | sort -u); do
modules+=("${path}")
done
echo "Checking modules: ${modules[*]}"
# Function to update the component status on Instatus
update_component_status() {
local component_status=$1
# see https://instatus.com/help/api/components
(curl -X PUT "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/components/$INSTATUS_COMPONENT_ID" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$component_status\"}")
}
# Function to create an incident
create_incident() {
local incident_name="Degraded Service"
local message="The following modules are experiencing issues:\n"
for i in "${!failures[@]}"; do
message+="$((i + 1)). ${failures[$i]}\n"
done
component_status="PARTIALOUTAGE"
if (( ${#failures[@]} == ${#modules[@]} )); then
component_status="MAJOROUTAGE"
fi
# see https://instatus.com/help/api/incidents
incident_id=$(curl -s -X POST "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$incident_name\",
\"message\": \"$message\",
\"components\": [\"$INSTATUS_COMPONENT_ID\"],
\"status\": \"INVESTIGATING\",
\"notify\": true,
\"statuses\": [
{
\"id\": \"$INSTATUS_COMPONENT_ID\",
\"status\": \"PARTIALOUTAGE\"
}
]
}" | jq -r '.id')
echo "Created incident with ID: $incident_id"
}
# Function to check for existing unresolved incidents
check_existing_incident() {
# Fetch the latest incidents with status not equal to "RESOLVED"
local unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
-H "Authorization: Bearer $INSTATUS_API_KEY" \
-H "Content-Type: application/json" | jq -r '.incidents[] | select(.status != "RESOLVED") | .id')
if [[ -n "$unresolved_incidents" ]]; then
echo "Unresolved incidents found: $unresolved_incidents"
return 0 # Indicate that there are unresolved incidents
else
echo "No unresolved incidents found."
return 1 # Indicate that no unresolved incidents exist
fi
}
force_redeploy_registry () {
# These are not secret values; safe to just expose directly in script
local VERCEL_TEAM_SLUG="codercom"
local VERCEL_TEAM_ID="team_tGkWfhEGGelkkqUUm9nXq17r"
local VERCEL_APP="registry"
local latest_res
latest_res=$(curl "https://api.vercel.com/v6/deployments?app=$VERCEL_APP&limit=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID&target=production&state=BUILDING,INITIALIZING,QUEUED,READY" \
--fail \
--silent \
--header "Authorization: Bearer $VERCEL_API_KEY" \
--header "Content-Type: application/json"
)
# If we have zero deployments, something is VERY wrong. Make the whole
# script exit with a non-zero status code
local latest_id
latest_id=$(echo "${latest_res}" | jq -r '.deployments[0].uid')
if [[ "${latest_id}" = "null" ]]; then
echo "Unable to pull any previous deployments for redeployment"
echo "Please redeploy the latest deployment manually in Vercel."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
local latest_date_ts_seconds
latest_date_ts_seconds=$(echo "${latest_res}" | jq -r '.deployments[0].createdAt/1000|floor')
local current_date_ts_seconds
current_date_ts_seconds="$(date +%s)"
local max_redeploy_interval_seconds=7200 # 2 hours
if (( current_date_ts_seconds - latest_date_ts_seconds < max_redeploy_interval_seconds )); then
echo "The registry was deployed less than 2 hours ago."
echo "Not automatically re-deploying the regitstry."
echo "A human reading this message should decide if a redeployment is necessary."
echo "Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
local latest_deployment_state
latest_deployment_state="$(echo "${latest_res}" | jq -r '.deployments[0].state')"
if [[ "${latest_deployment_state}" != "READY" ]]; then
echo "Last deployment was not in READY state. Skipping redeployment."
echo "A human reading this message should decide if a redeployment is necessary."
echo "Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
echo "============================================================="
echo "!!! Redeploying registry with deployment ID: ${latest_id} !!!"
echo "============================================================="
if ! curl -X POST "https://api.vercel.com/v13/deployments?forceNew=1&skipAutoDetectionConfirmation=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID" \
--fail \
--header "Authorization: Bearer $VERCEL_API_KEY" \
--header "Content-Type: application/json" \
--data-raw "{ \"deploymentId\": \"${latest_id}\", \"name\": \"${VERCEL_APP}\", \"target\": \"production\" }"; then
echo "DEPLOYMENT FAILED! Please check the Vercel dashboard for more information."
echo "https://vercel.com/codercom/registry/deployments"
exit 1
fi
}
# Check each module's accessibility
for module in "${modules[@]}"; do
# Trim leading/trailing whitespace from module name
module=$(echo "${module}" | xargs)
url="${REGISTRY_BASE_URL}/modules/${module}"
printf "=== Checking module %s at %s\n" "${module}" "${url}"
status_code=$(curl --output /dev/null --head --silent --fail --location "${url}" --retry 3 --write-out "%{http_code}")
if (( status_code != 200 )); then
printf "==> FAIL(%s)\n" "${status_code}"
status=1
failures+=("${module}")
else
printf "==> OK(%s)\n" "${status_code}"
fi
done
# Determine overall status and update Instatus component
if (( status == 0 )); then
echo "All modules are operational."
# set to
update_component_status "OPERATIONAL"
else
echo "The following modules have issues: ${failures[*]}"
# check if all modules are down
if (( ${#failures[@]} == ${#modules[@]} )); then
update_component_status "MAJOROUTAGE"
else
update_component_status "PARTIALOUTAGE"
fi
# Check if there is an existing incident before creating a new one
if ! check_existing_incident; then
create_incident
fi
# If a module is down, force a reployment to try getting things back online
# ASAP
# EDIT: registry.coder.com is no longer hosted on vercel
#force_redeploy_registry
fi
exit "${status}"

@ -0,0 +1,23 @@
name: Health
# Check modules health on registry.coder.com
on:
schedule:
- cron: "0,15,30,45 * * * *" # Runs every 15 minutes
workflow_dispatch: # Allows manual triggering of the workflow if needed
jobs:
run-script:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run check.sh
run: |
./.github/scripts/check.sh
env:
INSTATUS_API_KEY: ${{ secrets.INSTATUS_API_KEY }}
INSTATUS_PAGE_ID: ${{ secrets.INSTATUS_PAGE_ID }}
INSTATUS_COMPONENT_ID: ${{ secrets.INSTATUS_COMPONENT_ID }}
VERCEL_API_KEY: ${{ secrets.VERCEL_API_KEY }}

@ -16,14 +16,23 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: coder/coder/.github/actions/setup-tf@main
- uses: oven-sh/setup-bun@v2
- name: Check out code
uses: actions/checkout@v4
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
# We're using the latest version of Bun for now, but it might be worth
# reconsidering. They've pushed breaking changes in patch releases
# that have broken our CI.
# Our PR where issues started to pop up: https://github.com/coder/modules/pull/383
# The Bun PR that broke things: https://github.com/oven-sh/bun/pull/16067
bun-version: latest
- name: Setup
- name: Install dependencies
run: bun install
- run: bun test
- name: Run tests
run: bun test
pretty:
runs-on: ubuntu-latest
steps:

@ -0,0 +1,37 @@
name: deploy-registry
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
# Set id-token permission for gcloud
# Adding a comment because retriggering the build manually hung? I am the lord of devops and you will bend?
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935
with:
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
# For the time being, let's have the first couple merges to main in modules deploy a new version
# to *dev*. Once we review and make sure everything's working, we can deploy a new version to *main*.
# Maybe in the future we could automate this based on the result of E2E tests.
- name: Deploy to dev.registry.coder.com
run: |
gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev

5
.gitignore vendored

@ -1,4 +1,7 @@
.terraform*
node_modules
*.tfstate
*.tfstate.lock.info
*.tfstate.lock.info
# Ignore generated credentials from google-github-actions/auth
gha-creds-*.json

@ -0,0 +1 @@
<svg width="82" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-550 -124)"><g><g><g><g><path d="M551 124 631 124 631 204 551 204Z" fill="#ED7100" fill-rule="evenodd" fill-opacity="1"/><path d="M612.069 162.386C607.327 165.345 600.717 168.353 593.46 170.855 588.339 172.62 583.33 173.978 578.865 174.838 582.727 184.68 589.944 191.037 596.977 189.853 603.514 188.75 608.387 181.093 609.1 170.801L611.096 170.939C610.304 182.347 604.893 190.545 597.309 191.825 596.648 191.937 595.984 191.991 595.323 191.991 587.945 191.991 580.718 185.209 576.871 175.194 575.733 175.38 574.625 175.542 573.584 175.653 572.173 175.803 570.901 175.879 569.769 175.879 565.95 175.879 563.726 175.025 563.141 173.328 562.414 171.218 564.496 168.566 569.328 165.445L570.414 167.125C565.704 170.167 564.814 172.046 565.032 172.677 565.263 173.348 567.279 174.313 573.372 173.665 574.267 173.57 575.216 173.433 576.187 173.28 575.537 171.297 575.014 169.205 574.647 167.028 573.406 159.673 574.056 152.438 576.48 146.654 578.969 140.715 583.031 136.99 587.917 136.166 593.803 135.171 600.075 138.691 604.679 145.579L603.017 146.69C598.862 140.476 593.349 137.28 588.249 138.138 584.063 138.844 580.539 142.143 578.325 147.427 576.046 152.866 575.44 159.709 576.62 166.695 576.988 168.876 577.515 170.966 578.173 172.937 582.618 172.1 587.651 170.742 592.807 168.965 599.927 166.51 606.392 163.572 611.01 160.689 616.207 157.447 617.201 155.444 616.969 154.772 616.769 154.189 615.095 153.299 610.097 153.653L609.957 151.657C615.171 151.289 618.171 152.116 618.86 154.12 619.619 156.32 617.334 159.101 612.069 162.386" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="1"/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

@ -13,6 +13,7 @@ tags: [helper]
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
}
@ -28,6 +29,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
@ -45,6 +47,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id

@ -7,6 +7,7 @@
[![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder)
[![license](https://img.shields.io/github/license/coder/modules)](./LICENSE)
[![Health](https://github.com/coder/modules/actions/workflows/check.yaml/badge.svg)](https://github.com/coder/modules/actions/workflows/check.yaml)
</div>
@ -16,6 +17,7 @@ e.g.
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
agent_id = coder_agent.main.id

@ -0,0 +1,49 @@
---
display_name: Amazon DCV Windows
description: Amazon DCV Server and Web Client for Windows
icon: ../.icons/dcv.svg
maintainer_github: coder
verified: true
tags: [windows, amazon, dcv, web, desktop]
---
# Amazon DCV Windows
Amazon DCV is high performance remote display protocol that provides a secure way to deliver remote desktop and application streaming from any cloud or data center to any device, over varying network conditions.
![Amazon DCV on a Windows workspace](../.images/amazon-dcv-windows.png)
Enable DCV Server and Web Client on Windows workspaces.
```tf
module "dcv" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/amazon-dcv-windows/coder"
version = "1.0.24"
agent_id = resource.coder_agent.main.id
}
resource "coder_metadata" "dcv" {
count = data.coder_workspace.me.start_count
resource_id = aws_instance.dev.id # id of the instance resource
item {
key = "DCV client instructions"
value = "Run `coder port-forward ${data.coder_workspace.me.name} -p ${module.dcv[count.index].port}` and connect to **localhost:${module.dcv[count.index].port}${module.dcv[count.index].web_url_path}**"
}
item {
key = "username"
value = module.dcv[count.index].username
}
item {
key = "password"
value = module.dcv[count.index].password
sensitive = true
}
}
```
## License
Amazon DCV is free to use on AWS EC2 instances but requires a license for other cloud providers. Please see the instructions [here](https://docs.aws.amazon.com/dcv/latest/adminguide/setting-up-license.html#setting-up-license-ec2) for more information.

@ -0,0 +1,170 @@
# Terraform variables
$adminPassword = "${admin_password}"
$port = "${port}"
$webURLPath = "${web_url_path}"
function Set-LocalAdminUser {
Write-Output "[INFO] Starting Set-LocalAdminUser function"
$securePassword = ConvertTo-SecureString $adminPassword -AsPlainText -Force
Write-Output "[DEBUG] Secure password created"
Get-LocalUser -Name Administrator | Set-LocalUser -Password $securePassword
Write-Output "[INFO] Administrator password set"
Get-LocalUser -Name Administrator | Enable-LocalUser
Write-Output "[INFO] User Administrator enabled successfully"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
function Get-VirtualDisplayDriverRequired {
Write-Output "[INFO] Starting Get-VirtualDisplayDriverRequired function"
$token = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token-ttl-seconds' = '21600'} -Method PUT -Uri http://169.254.169.254/latest/api/token
Write-Output "[DEBUG] Token acquired: $token"
$instanceType = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token' = $token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-type
Write-Output "[DEBUG] Instance type: $instanceType"
$OSVersion = ((Get-ItemProperty -Path "Microsoft.PowerShell.Core\Registry::\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName) -replace "[^0-9]", ''
Write-Output "[DEBUG] OS version: $OSVersion"
# Force boolean result
$result = (($OSVersion -ne "2019") -and ($OSVersion -ne "2022") -and ($OSVersion -ne "2025")) -and (($instanceType[0] -ne 'g') -and ($instanceType[0] -ne 'p'))
Write-Output "[INFO] VirtualDisplayDriverRequired result: $result"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
return [bool]$result
}
function Download-DCV {
param (
[bool]$VirtualDisplayDriverRequired
)
Write-Output "[INFO] Starting Download-DCV function"
$downloads = @(
@{
Name = "DCV Display Driver"
Required = $VirtualDisplayDriverRequired
Path = "C:\Windows\Temp\DCVDisplayDriver.msi"
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-virtual-display-x64-Release.msi"
},
@{
Name = "DCV Server"
Required = $true
Path = "C:\Windows\Temp\DCVServer.msi"
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-server-x64-Release.msi"
}
)
foreach ($download in $downloads) {
if ($download.Required -and -not (Test-Path $download.Path)) {
try {
Write-Output "[INFO] Downloading $($download.Name)"
# Display progress manually (no events)
$progressActivity = "Downloading $($download.Name)"
$progressStatus = "Starting download..."
Write-Progress -Activity $progressActivity -Status $progressStatus -PercentComplete 0
# Synchronously download the file
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($download.Uri, $download.Path)
# Update progress
Write-Progress -Activity $progressActivity -Status "Completed" -PercentComplete 100
Write-Output "[INFO] $($download.Name) downloaded successfully."
} catch {
Write-Output "[ERROR] Failed to download $($download.Name): $_"
throw
}
} else {
Write-Output "[INFO] $($download.Name) already exists. Skipping download."
}
}
Write-Output "[INFO] All downloads completed"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
function Install-DCV {
param (
[bool]$VirtualDisplayDriverRequired
)
Write-Output "[INFO] Starting Install-DCV function"
if (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue)) {
if ($VirtualDisplayDriverRequired) {
Write-Output "[INFO] Installing DCV Display Driver"
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVDisplayDriver.msi /quiet /norestart" -Wait
} else {
Write-Output "[INFO] DCV Display Driver installation skipped (not required)."
}
Write-Output "[INFO] Installing DCV Server"
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVServer.msi ADDLOCAL=ALL /quiet /norestart /l*v C:\Windows\Temp\dcv_install_msi.log" -Wait
} else {
Write-Output "[INFO] DCV Server already installed, skipping installation."
}
# Wait for the service to appear with a timeout
$timeout = 10 # seconds
$elapsed = 0
while (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
Start-Sleep -Seconds 1
$elapsed++
}
if ($elapsed -ge $timeout) {
Write-Output "[WARNING] Timeout waiting for dcvserver service. A restart is required to complete installation."
Restart-SystemForDCV
} else {
Write-Output "[INFO] dcvserver service detected successfully."
}
}
function Restart-SystemForDCV {
Write-Output "[INFO] The system will restart in 10 seconds to finalize DCV installation."
Start-Sleep -Seconds 10
# Initiate restart
Restart-Computer -Force
# Exit the script after initiating restart
Write-Output "[INFO] Please wait for the system to restart..."
Exit 1
}
function Configure-DCV {
Write-Output "[INFO] Starting Configure-DCV function"
$dcvPath = "Microsoft.PowerShell.Core\Registry::\HKEY_USERS\S-1-5-18\Software\GSettings\com\nicesoftware\dcv"
# Create the required paths
@("$dcvPath\connectivity", "$dcvPath\session-management", "$dcvPath\session-management\automatic-console-session", "$dcvPath\display") | ForEach-Object {
if (-not (Test-Path $_)) {
New-Item -Path $_ -Force | Out-Null
}
}
# Set registry keys
New-ItemProperty -Path "$dcvPath\session-management" -Name create-session -PropertyType DWORD -Value 1 -Force
New-ItemProperty -Path "$dcvPath\session-management\automatic-console-session" -Name owner -Value Administrator -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name quic-port -PropertyType DWORD -Value $port -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-port -PropertyType DWORD -Value $port -Force
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-url-path -PropertyType String -Value $webURLPath -Force
# Attempt to restart service
if (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) {
Restart-Service -Name "dcvserver"
} else {
Write-Output "[WARNING] dcvserver service not found. Ensure the system was restarted properly."
}
Write-Output "[INFO] DCV configuration completed"
Read-Host "[DEBUG] Press Enter to proceed to the next step"
}
# Main Script Execution
Write-Output "[INFO] Starting script"
$VirtualDisplayDriverRequired = [bool](Get-VirtualDisplayDriverRequired)
Set-LocalAdminUser
Download-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
Install-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
Configure-DCV
Write-Output "[INFO] Script completed"

@ -0,0 +1,85 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "admin_password" {
type = string
default = "coderDCV!"
sensitive = true
}
variable "port" {
type = number
description = "The port number for the DCV server."
default = 8443
}
variable "subdomain" {
type = bool
description = "Whether to use a subdomain for the DCV server."
default = true
}
variable "slug" {
type = string
description = "The slug of the web-dcv coder_app resource."
default = "web-dcv"
}
resource "coder_app" "web-dcv" {
agent_id = var.agent_id
slug = var.slug
display_name = "Web DCV"
url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}"
icon = "/icon/dcv.svg"
subdomain = var.subdomain
}
resource "coder_script" "install-dcv" {
agent_id = var.agent_id
display_name = "Install DCV"
icon = "/icon/dcv.svg"
run_on_start = true
script = templatefile("${path.module}/install-dcv.ps1", {
admin_password : var.admin_password,
port : var.port,
web_url_path : local.web_url_path
})
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
web_url_path = var.subdomain ? "/" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
admin_username = "Administrator"
}
output "web_url_path" {
value = local.web_url_path
}
output "username" {
value = local.admin_username
}
output "password" {
value = var.admin_password
sensitive = true
}
output "port" {
value = var.port
}

@ -14,6 +14,7 @@ A module that adds Apache Airflow in your Coder template.
```tf
module "airflow" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.13"
agent_id = coder_agent.main.id

@ -16,6 +16,7 @@ Customize the preselected parameter value:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12"
default = "us-east-1"
@ -36,6 +37,7 @@ Change the display name and icon for a region using the corresponding maps:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12"
default = "ap-south-1"
@ -62,6 +64,7 @@ Hide the Asia Pacific regions Seoul and Osaka:
```tf
module "aws-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12"
exclude = ["ap-northeast-2", "ap-northeast-3"]

@ -13,6 +13,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele
```tf
module "azure_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12"
default = "eastus"
@ -33,6 +34,7 @@ Change the display name and icon for a region using the corresponding maps:
```tf
module "azure-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12"
custom_names = {
@ -56,6 +58,7 @@ Hide all regions in Australia except australiacentral:
```tf
module "azure-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12"
exclude = [

@ -13,8 +13,9 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@ -27,8 +28,9 @@ module "code-server" {
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
@ -40,8 +42,9 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@ -53,12 +56,13 @@ Enter the `<author>.<name>` into the extensions array and code-server will autom
### Pre-configure Settings
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@ -73,8 +77,9 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@ -88,8 +93,9 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@ -100,8 +106,9 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
offline = true
}

@ -39,7 +39,7 @@ variable "slug" {
}
variable "settings" {
type = map(string)
type = any
description = "A map of settings to apply to code-server."
default = {}
}

@ -104,7 +104,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
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)
# Use sed to remove single-line comments before parsing with jq
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
for extension in $extensions; do
if extension_installed "$extension"; then
continue

@ -13,6 +13,7 @@ Automatically logs the user into Coder when creating their workspace.
```tf
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder-login/coder"
version = "1.0.15"
agent_id = coder_agent.example.id

@ -15,6 +15,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder)
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/cursor/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
@ -27,6 +28,7 @@ module "cursor" {
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/cursor/coder"
version = "1.0.19"
agent_id = coder_agent.example.id

@ -17,8 +17,9 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
```tf
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@ -29,8 +30,9 @@ module "dotfiles" {
```tf
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@ -39,8 +41,9 @@ module "dotfiles" {
```tf
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
user = "root"
}
@ -50,14 +53,16 @@ module "dotfiles" {
```tf
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
@ -70,8 +75,9 @@ You can set a default dotfiles repository for all users by setting the `default_
```tf
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.18"
version = "1.0.29"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}

@ -1,4 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
DOTFILES_URI="${DOTFILES_URI}"
DOTFILES_USER="${DOTFILES_USER}"

@ -16,6 +16,7 @@ Customize the preselected parameter value:
```tf
module "exoscale-instance-type" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12"
default = "standard.medium"
@ -44,6 +45,7 @@ Change the display name a type using the corresponding maps:
```tf
module "exoscale-instance-type" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12"
default = "standard.medium"
@ -78,6 +80,7 @@ Show only gpu1 types
```tf
module "exoscale-instance-type" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12"
default = "gpu.large"

@ -16,6 +16,7 @@ Customize the preselected parameter value:
```tf
module "exoscale-zone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.12"
default = "ch-dk-2"
@ -43,6 +44,7 @@ Change the display name and icon for a zone using the corresponding maps:
```tf
module "exoscale-zone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.12"
default = "at-vie-1"

@ -13,8 +13,9 @@ A file browser for your workspace.
```tf
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@ -27,8 +28,9 @@ module "filebrowser" {
```tf
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@ -38,8 +40,9 @@ module "filebrowser" {
```tf
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
database_path = ".config/filebrowser.db"
}
@ -49,7 +52,9 @@ module "filebrowser" {
```tf
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.29"
agent_id = coder_agent.example.id
agent_name = "main"
subdomain = false

@ -39,7 +39,7 @@ describe("filebrowser", async () => {
"",
"📂 Serving /root at http://localhost:13339 ",
"",
"Running 'filebrowser --noauth --root /root --port 13339' ",
"Running 'filebrowser --noauth --root /root --port 13339 --baseurl ' ",
"",
"📝 Logs at /tmp/filebrowser.log",
]);
@ -61,7 +61,7 @@ describe("filebrowser", async () => {
"",
"📂 Serving /root at http://localhost:13339 ",
"",
"Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db' ",
"Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db --baseurl ' ",
"",
"📝 Logs at /tmp/filebrowser.log",
]);
@ -75,7 +75,7 @@ describe("filebrowser", async () => {
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"\u001B[0;1mInstalling filebrowser ",
"\u001b[0;1mInstalling filebrowser ",
"",
"🥳 Installation complete! ",
"",
@ -83,7 +83,7 @@ describe("filebrowser", async () => {
"",
"📂 Serving /home/coder/project at http://localhost:13339 ",
"",
"Running 'filebrowser --noauth --root /home/coder/project --port 13339' ",
"Running 'filebrowser --noauth --root /home/coder/project --port 13339 --baseurl ' ",
"",
"📝 Logs at /tmp/filebrowser.log",
]);
@ -106,7 +106,7 @@ describe("filebrowser", async () => {
"",
"📂 Serving /root at http://localhost:13339 ",
"",
"Running 'filebrowser --noauth --root /root --port 13339' ",
"Running 'filebrowser --noauth --root /root --port 13339 --baseurl /@default/default.main/apps/filebrowser' ",
"",
"📝 Logs at /tmp/filebrowser.log",
]);

@ -20,13 +20,8 @@ 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."
}
description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)"
default = null
}
variable "database_path" {
@ -73,6 +68,12 @@ variable "order" {
default = null
}
variable "slug" {
type = string
description = "The slug of the coder_app resource."
default = "filebrowser"
}
variable "subdomain" {
type = bool
description = <<-EOT
@ -85,7 +86,7 @@ variable "subdomain" {
resource "coder_script" "filebrowser" {
agent_id = var.agent_id
display_name = "File Browser"
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
icon = "/icon/filebrowser.svg"
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
PORT : var.port,
@ -93,18 +94,30 @@ resource "coder_script" "filebrowser" {
LOG_PATH : var.log_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),
SERVER_BASE_PATH : local.server_base_path
})
run_on_start = true
}
resource "coder_app" "filebrowser" {
agent_id = var.agent_id
slug = "filebrowser"
slug = var.slug
display_name = "File Browser"
url = "http://localhost:${var.port}"
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
url = local.url
icon = "/icon/filebrowser.svg"
subdomain = var.subdomain
share = var.share
order = var.order
healthcheck {
url = local.healthcheck_url
interval = 5
threshold = 6
}
}
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name != null ? ".${var.agent_name}" : "", var.slug)
url = "http://localhost:${var.port}${local.server_base_path}"
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
}

@ -1,9 +1,13 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
printf "$${BOLD}Installing filebrowser \n\n"
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
# Check if filebrowser is installed
if ! command -v filebrowser &> /dev/null; then
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
fi
printf "🥳 Installation complete! \n\n"
@ -17,13 +21,10 @@ 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"
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} --baseurl ${SERVER_BASE_PATH}' \n\n"
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} > ${LOG_PATH} 2>&1 &
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} --baseurl ${SERVER_BASE_PATH} > ${LOG_PATH} 2>&1 &
printf "📝 Logs at ${LOG_PATH} \n\n"

@ -15,6 +15,7 @@ We can use the simplest format here, only adding a default selection as the `atl
```tf
module "fly-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/fly-region/coder"
version = "1.0.2"
default = "atl"
@ -31,6 +32,7 @@ The regions argument can be used to display only the desired regions in the Code
```tf
module "fly-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/fly-region/coder"
version = "1.0.2"
default = "ams"
@ -46,6 +48,7 @@ Set custom icons and names with their respective maps.
```tf
module "fly-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/fly-region/coder"
version = "1.0.2"
default = "ams"

@ -13,6 +13,7 @@ This module adds Google Cloud Platform regions to your Coder template.
```tf
module "gcp_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12"
regions = ["us", "europe"]
@ -33,6 +34,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support,
```tf
module "gcp_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12"
default = ["us-west1-a"]
@ -49,6 +51,7 @@ resource "google_compute_instance" "example" {
```tf
module "gcp_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12"
regions = ["europe-west"]
@ -64,6 +67,7 @@ resource "google_compute_instance" "example" {
```tf
module "gcp_region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12"
regions = ["us", "europe"]

@ -13,6 +13,7 @@ This module allows you to automatically clone a repository by URL and skip if it
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -26,6 +27,7 @@ module "git-clone" {
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -40,6 +42,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -65,6 +68,7 @@ data "coder_parameter" "git_repo" {
# Clone the repository for branch `feat/example`
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -73,23 +77,24 @@ module "git_clone" {
# Create a code-server instance for the cloned repository
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone.folder_name}"
folder = "/home/${local.username}/${module.git_clone[count.index].folder_name}"
}
# Create a Coder app for the website
resource "coder_app" "website" {
count = data.coder_workspace.me.start_count
agent_id = coder_agent.example.id
order = 2
slug = "website"
external = true
display_name = module.git_clone.folder_name
url = module.git_clone.web_url
icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg"
count = module.git_clone.web_url != "" ? 1 : 0
display_name = module.git_clone[count.index].folder_name
url = module.git_clone[count.index].web_url
icon = module.git_clone[count.index].git_provider != "" ? "/icon/${module.git_clone[count.index].git_provider}.svg" : "/icon/git.svg"
}
```
@ -97,6 +102,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -115,6 +121,7 @@ To GitLab clone with a specific branch like `feat/example`
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -126,6 +133,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -146,6 +154,7 @@ For example, to clone the `feat/example` branch:
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id
@ -162,6 +171,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.18"
agent_id = coder_agent.example.id

@ -9,6 +9,9 @@ tags: [helper, git]
# git-commit-signing
> [!IMPORTANT]
> This module will only work with Git versions >=2.34, prior versions [do not support signing commits via SSH keys](https://lore.kernel.org/git/xmqq8rxpgwki.fsf@gitster.g/).
This module downloads your SSH key from Coder and uses it to sign commits with Git.
It requires `curl` and `jq` to be installed inside your workspace.
@ -18,6 +21,7 @@ This module has a chance of conflicting with the user's dotfiles / the personali
```tf
module "git-commit-signing" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-commit-signing/coder"
version = "1.0.11"
agent_id = coder_agent.example.id

@ -13,6 +13,7 @@ Runs a script that updates git credentials in the workspace to match the user's
```tf
module "git-config" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
@ -27,6 +28,7 @@ TODO: Add screenshot
```tf
module "git-config" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
@ -40,6 +42,7 @@ TODO: Add screenshot
```tf
module "git-config" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15"
agent_id = coder_agent.example.id

@ -13,6 +13,7 @@ Templates that utilize Github External Auth can automatically ensure that the Co
```tf
module "github-upload-public-key" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/github-upload-public-key/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
@ -45,6 +46,7 @@ data "coder_external_auth" "github" {
}
module "github-upload-public-key" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/github-upload-public-key/coder"
version = "1.0.15"
agent_id = coder_agent.example.id

@ -11,12 +11,15 @@ tags: [ide, jetbrains, helper, parameter]
This module adds a JetBrains Gateway Button to open any workspace with a single click.
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21"
version = "1.0.28"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
default = "GO"
@ -31,39 +34,64 @@ module "jetbrains_gateway" {
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21"
version = "1.0.28"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
}
```
### Use the latest release version
### Use the latest version of each IDE
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21"
version = "1.0.28"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
jetbrains_ides = ["IU", "PY"]
default = "IU"
latest = true
}
```
### Use fixed versions set by `jetbrains_ide_versions`
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.28"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
default = "IU"
latest = false
jetbrains_ide_versions = {
"IU" = {
build_number = "243.21565.193"
version = "2024.3"
}
"PY" = {
build_number = "243.21565.199"
version = "2024.3"
}
}
}
```
### Use the latest EAP version
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21"
version = "1.0.28"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
@ -72,15 +100,34 @@ module "jetbrains_gateway" {
}
```
### Custom base link
Due to the highest priority of the `ide_download_link` parameter in the `(jetbrains-gateway://...` within IDEA, the pre-configured download address will be overridden when using [IDEA's offline mode](https://www.jetbrains.com/help/idea/fully-offline-mode.html). Therefore, it is necessary to configure the `download_base_link` parameter for the `jetbrains_gateway` module to change the value of `ide_download_link`.
```tf
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.28"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
releases_base_link = "https://releases.internal.site/"
download_base_link = "https://download.internal.site/"
default = "GO"
}
```
## Supported IDEs
This module and JetBrains Gateway support the following JetBrains IDEs:
- GoLand (`GO`)
- WebStorm (`WS`)
- IntelliJ IDEA Ultimate (`IU`)
- PyCharm Professional (`PY`)
- PhpStorm (`PS`)
- CLion (`CL`)
- RubyMine (`RM`)
- Rider (`RD`)
- [GoLand (`GO`)](https://www.jetbrains.com/go/)
- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/)
- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/)
- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/)
- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/)
- [CLion (`CL`)](https://www.jetbrains.com/clion/)
- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/)
- [Rider (`RD`)](https://www.jetbrains.com/rider/)
- [RustRover (`RR`)](https://www.jetbrains.com/rust/)

@ -10,7 +10,6 @@ describe("jetbrains-gateway", async () => {
await testRequiredVariables(import.meta.dir, {
agent_id: "foo",
agent_name: "foo",
folder: "/home/foo",
});
@ -18,11 +17,10 @@ describe("jetbrains-gateway", 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",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz",
);
const coder_app = state.resources.find(
@ -37,7 +35,6 @@ describe("jetbrains-gateway", async () => {
it("default to first ide", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "foo",
folder: "/home/foo",
jetbrains_ides: '["IU", "GO", "PY"]',
});

@ -26,7 +26,9 @@ variable "slug" {
variable "agent_name" {
type = string
description = "Agent name."
description = "Agent name. (unused). Will be removed in a future version"
default = ""
}
variable "folder" {
@ -80,59 +82,63 @@ variable "jetbrains_ide_versions" {
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "241.14494.240"
version = "2024.1"
build_number = "243.21565.193"
version = "2024.3"
}
"PS" = {
build_number = "241.14494.237"
version = "2024.1"
build_number = "243.21565.202"
version = "2024.3"
}
"WS" = {
build_number = "241.14494.235"
version = "2024.1"
build_number = "243.21565.180"
version = "2024.3"
}
"PY" = {
build_number = "241.14494.241"
version = "2024.1"
build_number = "243.21565.199"
version = "2024.3"
}
"CL" = {
build_number = "241.14494.288"
build_number = "243.21565.238"
version = "2024.1"
}
"GO" = {
build_number = "241.14494.238"
version = "2024.1"
build_number = "243.21565.208"
version = "2024.3"
}
"RM" = {
build_number = "241.14494.234"
version = "2024.1"
build_number = "243.21565.197"
version = "2024.3"
}
"RD" = {
build_number = "241.14494.307"
version = "2024.1"
build_number = "243.21565.191"
version = "2024.3"
}
"RR" = {
build_number = "243.22562.230"
version = "2024.3"
}
}
validation {
condition = (
alltrue([
for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code)
])
)
error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}."
}
}
variable "jetbrains_ides" {
type = list(string)
description = "The list of IDE product codes."
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
validation {
condition = (
alltrue([
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code)
])
)
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}."
}
# check if the list is empty
validation {
@ -146,9 +152,29 @@ variable "jetbrains_ides" {
}
}
variable "releases_base_link" {
type = string
description = ""
default = "https://data.services.jetbrains.com"
validation {
condition = can(regex("^https?://.+$", var.releases_base_link))
error_message = "The releases_base_link must be a valid HTTP/S address."
}
}
variable "download_base_link" {
type = string
description = ""
default = "https://download.jetbrains.com"
validation {
condition = can(regex("^https?://.+$", var.download_base_link))
error_message = "The download_base_link must be a valid HTTP/S address."
}
}
data "http" "jetbrains_ide_versions" {
for_each = var.latest ? toset(var.jetbrains_ides) : toset([])
url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}"
url = "${var.releases_base_link}/products/releases?code=${each.key}&latest=true&type=${var.channel}"
}
locals {
@ -158,7 +184,7 @@ locals {
name = "GoLand",
identifier = "GO",
build_number = var.jetbrains_ide_versions["GO"].build_number,
download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
version = var.jetbrains_ide_versions["GO"].version
},
"WS" = {
@ -166,7 +192,7 @@ locals {
name = "WebStorm",
identifier = "WS",
build_number = var.jetbrains_ide_versions["WS"].build_number,
download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
version = var.jetbrains_ide_versions["WS"].version
},
"IU" = {
@ -174,7 +200,7 @@ locals {
name = "IntelliJ IDEA Ultimate",
identifier = "IU",
build_number = var.jetbrains_ide_versions["IU"].build_number,
download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
version = var.jetbrains_ide_versions["IU"].version
},
"PY" = {
@ -182,7 +208,7 @@ locals {
name = "PyCharm Professional",
identifier = "PY",
build_number = var.jetbrains_ide_versions["PY"].build_number,
download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
version = var.jetbrains_ide_versions["PY"].version
},
"CL" = {
@ -190,7 +216,7 @@ locals {
name = "CLion",
identifier = "CL",
build_number = var.jetbrains_ide_versions["CL"].build_number,
download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
version = var.jetbrains_ide_versions["CL"].version
},
"PS" = {
@ -198,7 +224,7 @@ locals {
name = "PhpStorm",
identifier = "PS",
build_number = var.jetbrains_ide_versions["PS"].build_number,
download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
version = var.jetbrains_ide_versions["PS"].version
},
"RM" = {
@ -206,16 +232,24 @@ locals {
name = "RubyMine",
identifier = "RM",
build_number = var.jetbrains_ide_versions["RM"].build_number,
download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
version = var.jetbrains_ide_versions["RM"].version
}
},
"RD" = {
icon = "/icon/rider.svg",
name = "Rider",
identifier = "RD",
build_number = var.jetbrains_ide_versions["RD"].build_number,
download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
version = var.jetbrains_ide_versions["RD"].version
},
"RR" = {
icon = "/icon/rustrover.svg",
name = "RustRover",
identifier = "RR",
build_number = var.jetbrains_ide_versions["RR"].build_number,
download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz"
version = var.jetbrains_ide_versions["RR"].version
}
}
@ -263,8 +297,6 @@ resource "coder_app" "gateway" {
data.coder_workspace.me.name,
"&owner=",
data.coder_workspace_owner.me.name,
"&agent=",
var.agent_name,
"&folder=",
var.folder,
"&url=",

@ -16,6 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
```tf
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
@ -44,6 +45,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
```tf
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
@ -72,6 +74,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
@ -95,8 +98,8 @@ provider "docker" {
# ...
registry_auth {
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username
password = module.jfrog.access_token
username = try(module.jfrog[0].username, "")
password = try(module.jfrog[0].access_token, "")
}
}
```

@ -15,6 +15,7 @@ A module that adds Jupyter Notebook in your Coder template.
```tf
module "jupyter-notebook" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jupyter-notebook/coder"
version = "1.0.19"
agent_id = coder_agent.example.id

@ -15,8 +15,9 @@ A module that adds JupyterLab in your Coder template.
```tf
module "jupyterlab" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jupyterlab/coder"
version = "1.0.22"
version = "1.0.23"
agent_id = coder_agent.example.id
}
```

@ -9,7 +9,7 @@ BOLD='\033[0;1m'
printf "$${BOLD}Installing jupyterlab!\n"
# check if jupyterlab is installed
if ! command -v jupyterlab > /dev/null 2>&1; then
if ! command -v jupyter-lab > /dev/null 2>&1; then
# install jupyterlab
# check if pipx is installed
if ! command -v pipx > /dev/null 2>&1; then

@ -13,8 +13,9 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
```tf
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/kasmvnc/coder"
version = "1.0.22"
version = "1.0.23"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
}

@ -42,7 +42,7 @@ resource "coder_script" "kasm_vnc" {
script = templatefile("${path.module}/run.sh", {
PORT : var.port,
DESKTOP_ENVIRONMENT : var.desktop_environment,
VERSION : var.kasm_version
KASM_VERSION : var.kasm_version
})
run_on_start = true
}

@ -1,6 +1,7 @@
#!/usr/bin/env bash
#!/bin/bash
# Exit on error, undefined variables, and pipe failures
set -euo pipefail
# Function to check if vncserver is already installed
check_installed() {
@ -14,143 +15,170 @@ check_installed() {
# 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
local url="$1"
local output="$2"
local download_tool
if command -v curl &> /dev/null; then
# shellcheck disable=SC2034
download_tool=(curl -fsSL)
elif command -v wget &> /dev/null; then
# shellcheck disable=SC2034
download_tool=(wget -q -O-)
elif command -v busybox &> /dev/null; then
busybox wget -O $output $url
# shellcheck disable=SC2034
download_tool=(busybox wget -O-)
else
echo "Neither wget, curl, nor busybox is installed. Please install one of them to proceed."
echo "ERROR: No download tool available (curl, wget, or busybox required)"
exit 1
fi
# shellcheck disable=SC2288
"$${download_tool[@]}" "$url" > "$output" || {
echo "ERROR: Failed to download $url"
exit 1
}
}
# 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
}
local kasmdeb="/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
}
download_file "$url" "$kasmdeb"
# 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
CACHE_DIR="/var/lib/apt/lists/partial"
# Check if the directory exists and was modified in the last 60 minutes
if [[ ! -d "$CACHE_DIR" ]] || ! find "$CACHE_DIR" -mmin -60 -print -quit &> /dev/null; then
echo "Stale package cache, updating..."
# Update package cache with a 300-second timeout for dpkg lock
sudo apt-get -o DPkg::Lock::Timeout=300 -qq update
fi
echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list
sudo apt-get update
sudo apt-get install libssl1.1
DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests "$kasmdeb"
rm "$kasmdeb"
}
# 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
local kasmrpm="/tmp/kasmvncserver.rpm"
local package_manager
if command -v dnf &> /dev/null; then
# shellcheck disable=SC2034
package_manager=(dnf localinstall -y)
elif command -v zypper &> /dev/null; then
# shellcheck disable=SC2034
package_manager=(zypper install -y)
elif command -v yum &> /dev/null; then
# shellcheck disable=SC2034
package_manager=(yum localinstall -y)
elif command -v rpm &> /dev/null; then
# Do we need to manually handle missing dependencies?
# shellcheck disable=SC2034
package_manager=(rpm -i)
else
echo "ERROR: No supported package manager available (dnf, zypper, yum, or rpm required)"
exit 1
fi
download_file "$url" "$kasmrpm"
# shellcheck disable=SC2288
sudo "$${package_manager[@]}" "$kasmrpm" || {
echo "ERROR: Failed to install $kasmrpm"
exit 1
}
rm "$kasmrpm"
}
# 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
local kasmtgz="/tmp/kasmvncserver.tgz"
download_file "$url" "$kasmtgz"
tar -xzf "$kasmtgz" -C /usr/local/bin/
rm "$kasmtgz"
}
# 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)
if [[ ! -f /etc/os-release ]]; then
echo "ERROR: Cannot detect OS: /etc/os-release not found"
exit 1
fi
# shellcheck disable=SC1091
source /etc/os-release
distro="$ID"
distro_version="$VERSION_ID"
codename="$VERSION_CODENAME"
arch="$(uname -m)"
if [[ "$ID" == "ol" ]]; then
distro="oracle"
distro_version="$${distro_version%%.*}"
elif [[ "$ID" == "fedora" ]]; then
distro_version="$(grep -oP '\(\K[\w ]+' /etc/fedora-release | tr '[:upper:]' '[:lower:]' | tr -d ' ')"
fi
echo "Detected Distribution: $distro"
echo "Detected Version: $version"
echo "Detected Version: $distro_version"
echo "Detected Codename: $codename"
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
case "$arch" in
x86_64)
if [[ "$distro" =~ ^(ubuntu|debian|kali)$ ]]; then
arch="amd64"
fi
;;
aarch64)
if [[ "$distro" =~ ^(ubuntu|debian|kali)$ ]]; then
arch="arm64"
fi
;;
arm64)
: # This is effectively a noop
;;
*)
echo "ERROR: Unsupported architecture: $arch"
exit 1
;;
esac
# Check if vncserver is installed, and install if not
if ! check_installed; then
echo "Installing KASM version: ${VERSION}"
# Check for NOPASSWD sudo (required)
if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then
echo "ERROR: sudo NOPASSWD access required!"
exit 1
fi
#base_url="https://github.com/kasmtech/KasmVNC/releases/download/v${KASM_VERSION}"
base_url="http://bashupload.com/boJuH"
bin_name="2w79N.deb"
echo "Installing KASM version: ${KASM_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
#bin_name="kasmvncserver_$${codename}_${KASM_VERSION}_$${arch}.deb"
install_deb "$base_url/$bin_name"
;;
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
oracle | fedora | opensuse)
bin_name="kasmvncserver_$${distro}_$${distro_version}_${KASM_VERSION}_$${arch}.rpm"
install_rpm "$base_url/$bin_name"
;;
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"
bin_name="kasmvnc.alpine_$${distro_version//./}_$${arch}.tgz"
install_alpine "$base_url/$bin_name"
;;
*)
echo "Unsupported distribution: $${distro}"
echo "Unsupported distribution: $distro"
exit 1
;;
esac
@ -158,22 +186,53 @@ else
echo "vncserver already installed. Skipping installation."
fi
# Coder port-forwarding from dashboard only supports HTTP
sudo bash -c "cat > /etc/kasmvnc/kasmvnc.yaml <<EOF
if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
kasm_config_file="/etc/kasmvnc/kasmvnc.yaml"
SUDO=sudo
else
kasm_config_file="$HOME/.vnc/kasmvnc.yaml"
SUDO=
echo "WARNING: Sudo access not available, using user config dir!"
if [[ -f "$kasm_config_file" ]]; then
echo "WARNING: Custom user KasmVNC config exists, not overwriting!"
echo "WARNING: Ensure that you manually configure the appropriate settings."
kasm_config_file="/dev/stderr"
else
echo "WARNING: This may prevent custom user KasmVNC settings from applying!"
mkdir -p "$HOME/.vnc"
fi
fi
echo "Writing KasmVNC config to $kasm_config_file"
$SUDO tee "$kasm_config_file" > /dev/null << EOF
network:
protocol: http
websocket_port: ${PORT}
ssl:
require_ssl: false
pem_certificate:
pem_key:
udp:
public_ip: 127.0.0.1
EOF"
EOF
# This password is not used since we start the server without auth.
# The server is protected via the Coder session token / tunnel
# and does not listen publicly
echo -e "password\npassword\n" | vncpasswd -wo -u $USER
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
# Start the server
printf "🚀 Starting KasmVNC server...\n"
sudo -u $USER bash -c "vncserver -select-de ${DESKTOP_ENVIRONMENT} -disableBasicAuth" > /tmp/kasmvncserver.log 2>&1 &
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 &
pid=$!
# Wait for server to start
sleep 5
grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10
if ps -p $pid | grep -q "^$pid"; then
echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log"
exit 1
fi
printf "🚀 KasmVNC server started successfully!\n"

@ -13,6 +13,7 @@ Automatically installs [Node.js](https://github.com/nodejs/node) via [nvm](https
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
@ -25,6 +26,7 @@ This installs multiple versions of Node.js:
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
@ -43,6 +45,7 @@ A example with all available options:
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.10"
agent_id = coder_agent.example.id

@ -13,6 +13,7 @@ Run a script on workspace start that allows developers to run custom commands to
```tf
module "personalize" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/personalize/coder"
version = "1.0.2"
agent_id = coder_agent.example.id

@ -25,7 +25,7 @@ const removeOldContainers = async () => {
"-a",
"-q",
"--filter",
`label=modules-test`,
"label=modules-test",
]);
let containerIDsRaw = await readableStreamToText(proc.stdout);
let exitCode = await proc.exited;

@ -56,6 +56,7 @@ slackme npm run long-build
```tf
module "slackme" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
@ -72,6 +73,7 @@ slackme npm run long-build
```tf
module "slackme" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.2"
agent_id = coder_agent.example.id

@ -194,13 +194,18 @@ export const testRequiredVariables = <TVars extends TerraformVariables>(
export const runTerraformApply = async <TVars extends TerraformVariables>(
dir: string,
vars: Readonly<TVars>,
env?: Record<string, string>,
customEnv?: Record<string, string>,
): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const combinedEnv = env === undefined ? {} : { ...env };
for (const [key, value] of Object.entries(vars)) {
combinedEnv[`TF_VAR_${key}`] = String(value);
const childEnv: Record<string, string | undefined> = {
...process.env,
...(customEnv ?? {}),
};
for (const [key, value] of Object.entries(vars) as [string, JsonValue][]) {
if (value !== null) {
childEnv[`TF_VAR_${key}`] = String(value);
}
}
const proc = spawn(
@ -216,7 +221,7 @@ export const runTerraformApply = async <TVars extends TerraformVariables>(
],
{
cwd: dir,
env: combinedEnv,
env: childEnv,
stderr: "pipe",
stdout: "pipe",
},

@ -21,14 +21,39 @@ for dir in "${changed_dirs[@]}"; do
if [[ -f "$dir/README.md" ]]; then
file="$dir/README.md"
tmpfile=$(mktemp /tmp/tempfile.XXXXXX)
awk -v tag="$LATEST_TAG" '{
if ($1 == "version" && $2 == "=") {
sub(/"[^"]*"/, "\"" tag "\"")
print
} else {
awk -v tag="$LATEST_TAG" '
BEGIN { in_code_block = 0; in_nested_block = 0 }
{
# Detect the start and end of Markdown code blocks.
if ($0 ~ /^```/) {
in_code_block = !in_code_block
# Reset nested block tracking when exiting a code block.
if (!in_code_block) {
in_nested_block = 0
}
}
# Handle nested blocks within a code block.
if (in_code_block) {
# Detect the start of a nested block (skipping "module" blocks).
if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) {
in_nested_block++
}
# Detect the end of a nested block.
if ($0 ~ /}/ && in_nested_block > 0) {
in_nested_block--
}
# Update "version" only if not in a nested block.
if (!in_nested_block && $1 == "version" && $2 == "=") {
sub(/"[^"]*"/, "\"" tag "\"")
}
}
print
}
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
# Check if the README.md file has changed
if ! git diff --quiet -- "$dir/README.md"; then

@ -14,6 +14,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
@ -45,6 +46,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
@ -57,6 +59,7 @@ module "vault" {
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
@ -70,6 +73,7 @@ module "vault" {
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id

@ -10,10 +10,11 @@ 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.
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/users/oidc-auth) 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" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-jwt/coder"
version = "1.0.20"
agent_id = coder_agent.example.id
@ -40,6 +41,7 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-jwt/coder"
version = "1.0.20"
agent_id = coder_agent.example.id
@ -55,6 +57,7 @@ module "vault" {
data "coder_workspace_owner" "me" {}
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-jwt/coder"
version = "1.0.20"
agent_id = coder_agent.example.id
@ -67,6 +70,7 @@ module "vault" {
```tf
module "vault" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vault-jwt/coder"
version = "1.0.20"
agent_id = coder_agent.example.id

@ -15,6 +15,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
```tf
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
@ -27,6 +28,7 @@ module "vscode" {
```tf
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.15"
agent_id = coder_agent.example.id

@ -13,8 +13,9 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
accept_license = true
}
@ -28,8 +29,9 @@ module "vscode-web" {
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@ -41,8 +43,9 @@ module "vscode-web" {
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@ -51,12 +54,13 @@ module "vscode-web" {
### Pre-configure Settings
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
```tf
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.22"
version = "1.0.29"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {

@ -92,7 +92,7 @@ variable "order" {
}
variable "settings" {
type = map(string)
type = any
description = "A map of settings to apply to VS Code web."
default = {}
}

@ -92,7 +92,8 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
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)
# Use sed to remove single-line comments before parsing with jq
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done

@ -14,9 +14,9 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
```tf
# AWS example. See below for examples of using this module with other providers
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/windows-rdp/coder"
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
}
@ -32,9 +32,9 @@ module "windows_rdp" {
```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/windows-rdp/coder"
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
}
@ -44,9 +44,9 @@ module "windows_rdp" {
```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/windows-rdp/coder"
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
}

Loading…
Cancel
Save