Compare commits

...

19 Commits

Author SHA1 Message Date
Muhammad Atif Ali
b88d50a7c9 Merge branch 'main' into atif/multi-gateway 2024-11-27 13:21:55 +05:00
Muhammad Atif Ali
b345e62ac1 feat: add Amazon DCV Windows module (#345) 2024-11-27 10:29:02 +05:00
Muhammad Atif Ali
cd6aa274f1 fix tests 2024-11-26 14:08:24 +05:00
Muhammad Atif Ali
2f51d70fb7 always use latest and update default versions to 2024.3 2024-11-26 13:50:55 +05:00
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>
2024-11-22 23:45:57 +00:00
Muhammad Atif Ali
5101c27c83 chore: integrate Instatus in check script (#342) 2024-11-19 14:22:03 +05:00
Muhammad Atif Ali
90bfbfdc40 chore: add health check badge (#341) 2024-11-18 16:11:12 +05:00
Muhammad Atif Ali
dbf3c47f45 Merge branch 'main' into atif/multi-gateway 2024-11-17 00:00:33 +05:00
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
2024-11-15 12:27:03 +00:00
Muhammad Atif Ali
d45f2e6ad1 Update JetBrains Gateway module to v1.0.24 2024-11-14 20:06:39 +05:00
Muhammad Atif Ali
70020d8b8c Support multiple default IDEs in JetBrains Gateway 2024-11-14 19:54:58 +05:00
Muhammad Atif Ali
937ffcd47b Update slug format for JetBrains Gateway apps
This change improves URL uniqueness by appending a lowercase IDE
identifier to the slug, ensuring distinct slugs for each default IDE.
2024-11-14 19:50:09 +05:00
Muhammad Atif Ali
5bc2aa4aa0 Fix JetBrains Gateway tests for multiple IDEs
- Allow creation of links with multiple IDEs.
- Ensure outputs handle arrays for identifying multiple IDEs.
- Update runTerraformApply to handle array values as JSON strings.
2024-11-14 19:36:36 +05:00
Muhammad Atif Ali
4452630a7e Support multiple default IDEs in JetBrains Gateway 2024-11-14 18:41:40 +05:00
Muhammad Atif Ali
27e3faf31c feat: enable multiple IDE buttons in JetBrains
Add support for specifying a list of default IDEs to be displayed on
the Workspace page. This allows users to see multiple IDE options
simultaneously. Ensure no duplicates are included and validate
provided IDE codes against allowed set. Adjust logic to dynamically
render IDE buttons based on specified defaults, improving flexibility
in user interface setup.
2024-11-14 11:31:27 +05:00
Tao Chen
f5ab7995d1 feat(filebrowser): check if already installed (#334)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-10-30 18:10:26 +05:00
djarbz
528a8a9fea fix(kasmvnc): optimize KasmVNC deployment script (#329)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-10-30 10:25:41 +00:00
Kerwin Bryant
87854707bc feat(jetbrains-gateway): add releases_base_link/download_base_link variables (#333) 2024-10-30 14:51:03 +05:00
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.
2024-10-23 13:51:25 -07:00
20 changed files with 897 additions and 197 deletions

186
.github/scripts/check.sh vendored Executable file
View File

@@ -0,0 +1,186 @@
#!/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="Testing Instatus"
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
response=$(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\"
}
]
}")
incident_id=$(echo "$response" | jq -r '.id')
echo "$incident_id"
}
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
# Create a new incident
incident_id=$(create_incident)
echo "Created incident with ID: $incident_id"
# If a module is down, force a reployment to try getting things back online
# ASAP
force_redeploy_registry
fi
exit "${status}"

23
.github/workflows/check.yaml vendored Normal file
View File

@@ -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 }}

1
.icons/dcv.svg Normal file
View File

@@ -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

View File

@@ -7,6 +7,7 @@
[![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder) [![discord](https://img.shields.io/discord/747933592273027093?label=discord)](https://discord.gg/coder)
[![license](https://img.shields.io/github/license/coder/modules)](./LICENSE) [![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> </div>

View File

@@ -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.

View File

@@ -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"

View File

@@ -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
}

View File

@@ -14,7 +14,7 @@ A file browser for your workspace.
```tf ```tf
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22" version = "1.0.23"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -28,7 +28,7 @@ module "filebrowser" {
```tf ```tf
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22" version = "1.0.23"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@@ -39,7 +39,7 @@ module "filebrowser" {
```tf ```tf
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "registry.coder.com/modules/filebrowser/coder"
version = "1.0.22" version = "1.0.23"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
database_path = ".config/filebrowser.db" database_path = ".config/filebrowser.db"
} }

View File

@@ -20,13 +20,8 @@ data "coder_workspace_owner" "me" {}
variable "agent_name" { variable "agent_name" {
type = string type = string
description = "The name of the main deployment. (Used to build the subpath for coder_app.)" description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)"
default = "" default = null
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" { variable "database_path" {
@@ -73,6 +68,12 @@ variable "order" {
default = null default = null
} }
variable "slug" {
type = string
description = "The slug of the coder_app resource."
default = "filebrowser"
}
variable "subdomain" { variable "subdomain" {
type = bool type = bool
description = <<-EOT description = <<-EOT
@@ -85,7 +86,7 @@ variable "subdomain" {
resource "coder_script" "filebrowser" { resource "coder_script" "filebrowser" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "File Browser" 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", { script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
PORT : var.port, PORT : var.port,
@@ -93,18 +94,30 @@ resource "coder_script" "filebrowser" {
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
DB_PATH : var.database_path, DB_PATH : var.database_path,
SUBDOMAIN : var.subdomain, 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 run_on_start = true
} }
resource "coder_app" "filebrowser" { resource "coder_app" "filebrowser" {
agent_id = var.agent_id agent_id = var.agent_id
slug = "filebrowser" slug = var.slug
display_name = "File Browser" display_name = "File Browser"
url = "http://localhost:${var.port}" url = local.url
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" icon = "/icon/filebrowser.svg"
subdomain = var.subdomain subdomain = var.subdomain
share = var.share share = var.share
order = var.order 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"
}

View File

@@ -1,9 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
BOLD='\033[0;1m' BOLD='\033[0;1m'
printf "$${BOLD}Installing filebrowser \n\n" 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" printf "🥳 Installation complete! \n\n"

View File

@@ -14,12 +14,12 @@ This module adds a JetBrains Gateway Button to open any workspace with a single
```tf ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21" version = "1.0.24"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example" agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"] jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
default = "GO" default = ["GO"]
} }
``` ```
@@ -32,27 +32,27 @@ module "jetbrains_gateway" {
```tf ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21" version = "1.0.24"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example" agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"] jetbrains_ides = ["GO", "WS"]
default = "GO" default = ["GO"]
} }
``` ```
### Use the latest release version ### Use the fixed version
```tf ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21" version = "1.0.24"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example" agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"] jetbrains_ides = ["GO", "WS"]
default = "GO" default = ["GO"]
latest = true latest = false # current version is 2024.3
} }
``` ```
@@ -61,17 +61,50 @@ module "jetbrains_gateway" {
```tf ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.21" version = "1.0.24"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example" agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"] jetbrains_ides = ["GO", "WS"]
default = "GO" default = ["GO"]
latest = true latest = true
channel = "eap" channel = "eap"
} }
``` ```
### 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" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.24"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
releases_base_link = "https://releases.internal.site/"
download_base_link = "https://download.internal.site/"
default = ["GO"]
}
```
### Add multiple IDEs
**Note:** This removes the choice of IDE from the user.
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.24"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
default = ["GO", "WS"]
}
```
## Supported IDEs ## Supported IDEs
This module and JetBrains Gateway support the following JetBrains IDEs: This module and JetBrains Gateway support the following JetBrains IDEs:

View File

@@ -16,14 +16,13 @@ describe("jetbrains-gateway", async () => {
it("should create a link with the default values", async () => { it("should create a link with the default values", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
// These are all required.
agent_id: "foo", agent_id: "foo",
agent_name: "foo", agent_name: "foo",
folder: "/home/coder", folder: "/home/coder",
}); });
expect(state.outputs.url.value).toBe( expect(state.outputs.url.value).toEqual([
"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&agent=foo&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( const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "gateway", (res) => res.type === "coder_app" && res.name === "gateway",
@@ -34,13 +33,31 @@ describe("jetbrains-gateway", async () => {
expect(coder_app?.instances[0].attributes.order).toBeNull(); expect(coder_app?.instances[0].attributes.order).toBeNull();
}); });
it("default to first ide", async () => { it("default to first IDE", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
agent_name: "foo", agent_name: "foo",
folder: "/home/foo", folder: "/home/foo",
jetbrains_ides: '["IU", "GO", "PY"]', jetbrains_ides: ["IU", "PY"],
}); });
expect(state.outputs.identifier.value).toBe("IU"); expect(state.outputs.identifier.value).toEqual(["IU"]);
expect(state.outputs.url.value).toEqual([
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&agent=foo&folder=/home/foo&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",
]);
});
it("should create multiple IDEs", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "foo",
folder: "/home/foo",
default: ["GO", "IU", "PY"],
});
expect(state.outputs.identifier.value).toEqual(["GO", "IU", "PY"]);
expect(state.outputs.url.value).toEqual([
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&agent=foo&folder=/home/foo&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=GO&ide_build_number=243.21565.208&ide_download_link=https://download.jetbrains.com/go/goland-2024.3.tar.gz",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&agent=foo&folder=/home/foo&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",
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&agent=foo&folder=/home/foo&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=PY&ide_build_number=243.21565.199&ide_download_link=https://download.jetbrains.com/python/pycharm-professional-2024.3.tar.gz",
]);
}); });
}); });

View File

@@ -39,9 +39,23 @@ variable "folder" {
} }
variable "default" { variable "default" {
default = "" default = []
type = string type = list(string)
description = "Default IDE" description = "List of default IDEs to be added to the Workspace page."
# check if the list is unique
validation {
condition = length(var.default) == length(toset(var.default))
error_message = "The default must not contain duplicates."
}
# check if default are valid jetbrains_ides
validation {
condition = (
alltrue([
for code in var.default : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
])
)
error_message = "The default must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
}
} }
variable "order" { variable "order" {
@@ -59,7 +73,7 @@ variable "coder_parameter_order" {
variable "latest" { variable "latest" {
type = bool type = bool
description = "Whether to fetch the latest version of the IDE." description = "Whether to fetch the latest version of the IDE."
default = false default = true
} }
variable "channel" { variable "channel" {
@@ -80,36 +94,36 @@ variable "jetbrains_ide_versions" {
description = "The set of versions for each jetbrains IDE" description = "The set of versions for each jetbrains IDE"
default = { default = {
"IU" = { "IU" = {
build_number = "241.14494.240" build_number = "243.21565.193"
version = "2024.1" version = "2024.3"
} }
"PS" = { "PS" = {
build_number = "241.14494.237" build_number = "243.21565.202"
version = "2024.1" version = "2024.3"
} }
"WS" = { "WS" = {
build_number = "241.14494.235" build_number = "243.21565.180"
version = "2024.1" version = "2024.3"
} }
"PY" = { "PY" = {
build_number = "241.14494.241" build_number = "243.21565.199"
version = "2024.1" version = "2024.3"
} }
"CL" = { "CL" = {
build_number = "241.14494.288" build_number = "243.21565.238"
version = "2024.1" version = "2024.1"
} }
"GO" = { "GO" = {
build_number = "241.14494.238" build_number = "243.21565.208"
version = "2024.1" version = "2024.3"
} }
"RM" = { "RM" = {
build_number = "241.14494.234" build_number = "243.21565.197"
version = "2024.1" version = "2024.3"
} }
"RD" = { "RD" = {
build_number = "241.14494.307" build_number = "243.21565.191"
version = "2024.1" version = "2024.3"
} }
} }
validation { validation {
@@ -124,7 +138,7 @@ variable "jetbrains_ide_versions" {
variable "jetbrains_ides" { variable "jetbrains_ides" {
type = list(string) type = list(string)
description = "The list of IDE product codes." description = "The list of IDE product codes to be shown to the user. Does not apply when there are multiple defaults."
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"] default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
validation { validation {
condition = ( condition = (
@@ -146,9 +160,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" { data "http" "jetbrains_ide_versions" {
for_each = var.latest ? toset(var.jetbrains_ides) : toset([]) 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 { locals {
@@ -158,7 +192,7 @@ locals {
name = "GoLand", name = "GoLand",
identifier = "GO", identifier = "GO",
build_number = var.jetbrains_ide_versions["GO"].build_number, 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 version = var.jetbrains_ide_versions["GO"].version
}, },
"WS" = { "WS" = {
@@ -166,7 +200,7 @@ locals {
name = "WebStorm", name = "WebStorm",
identifier = "WS", identifier = "WS",
build_number = var.jetbrains_ide_versions["WS"].build_number, 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 version = var.jetbrains_ide_versions["WS"].version
}, },
"IU" = { "IU" = {
@@ -174,7 +208,7 @@ locals {
name = "IntelliJ IDEA Ultimate", name = "IntelliJ IDEA Ultimate",
identifier = "IU", identifier = "IU",
build_number = var.jetbrains_ide_versions["IU"].build_number, 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 version = var.jetbrains_ide_versions["IU"].version
}, },
"PY" = { "PY" = {
@@ -182,7 +216,7 @@ locals {
name = "PyCharm Professional", name = "PyCharm Professional",
identifier = "PY", identifier = "PY",
build_number = var.jetbrains_ide_versions["PY"].build_number, 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 version = var.jetbrains_ide_versions["PY"].version
}, },
"CL" = { "CL" = {
@@ -190,7 +224,7 @@ locals {
name = "CLion", name = "CLion",
identifier = "CL", identifier = "CL",
build_number = var.jetbrains_ide_versions["CL"].build_number, 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 version = var.jetbrains_ide_versions["CL"].version
}, },
"PS" = { "PS" = {
@@ -198,7 +232,7 @@ locals {
name = "PhpStorm", name = "PhpStorm",
identifier = "PS", identifier = "PS",
build_number = var.jetbrains_ide_versions["PS"].build_number, 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 version = var.jetbrains_ide_versions["PS"].version
}, },
"RM" = { "RM" = {
@@ -206,7 +240,7 @@ locals {
name = "RubyMine", name = "RubyMine",
identifier = "RM", identifier = "RM",
build_number = var.jetbrains_ide_versions["RM"].build_number, 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 version = var.jetbrains_ide_versions["RM"].version
} }
"RD" = { "RD" = {
@@ -214,28 +248,47 @@ locals {
name = "Rider", name = "Rider",
identifier = "RD", identifier = "RD",
build_number = var.jetbrains_ide_versions["RD"].build_number, 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 version = var.jetbrains_ide_versions["RD"].version
} }
} }
icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon identifier = try([data.coder_parameter.jetbrains_ide[0].value], var.default)
json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {} list_json_data = var.latest ? [
key = var.latest ? keys(local.json_data)[0] : "" for ide in local.identifier : jsondecode(data.http.jetbrains_ide_versions[ide].response_body)
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name ] : []
identifier = data.coder_parameter.jetbrains_ide.value list_key = var.latest ? [
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link for j in local.list_json_data : keys(j)[0]
build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number ] : []
version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version download_links = length(local.list_key) > 0 ? [
for i, j in local.list_json_data : j[local.list_key[i]][0].downloads.linux.link
] : [
for ide in local.identifier : local.jetbrains_ides[ide].download_link
]
build_numbers = length(local.list_key) > 0 ? [
for i, j in local.list_json_data : j[local.list_key[i]][0].build
] : [
for ide in local.identifier : local.jetbrains_ides[ide].build_number
]
versions = length(local.list_key) > 0 ? [
for i, j in local.list_json_data : j[local.list_key[i]][0].version
] : [
for ide in local.identifier : local.jetbrains_ides[ide].version
]
display_names = [for key in keys(coder_app.gateway) : coder_app.gateway[key].display_name]
icons = [for key in keys(coder_app.gateway) : coder_app.gateway[key].icon]
urls = [for key in keys(coder_app.gateway) : coder_app.gateway[key].url]
} }
data "coder_parameter" "jetbrains_ide" { data "coder_parameter" "jetbrains_ide" {
# remove the coder_parameter if there are multiple default
count = length(var.default) > 1 ? 0 : 1
type = "string" type = "string"
name = "jetbrains_ide" name = "jetbrains_ide"
display_name = "JetBrains IDE" display_name = "JetBrains IDE"
icon = "/icon/gateway.svg" icon = "/icon/gateway.svg"
mutable = true mutable = true
default = var.default == "" ? var.jetbrains_ides[0] : var.default default = length(var.default) > 0 ? var.default[0] : var.jetbrains_ides[0]
order = var.coder_parameter_order order = var.coder_parameter_order
dynamic "option" { dynamic "option" {
@@ -252,10 +305,11 @@ data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {} data "coder_workspace_owner" "me" {}
resource "coder_app" "gateway" { resource "coder_app" "gateway" {
for_each = length(var.default) > 1 ? toset(var.default) : toset([data.coder_parameter.jetbrains_ide[0].value])
agent_id = var.agent_id agent_id = var.agent_id
slug = var.slug slug = "${var.slug}-${lower(each.value)}"
display_name = local.display_name display_name = local.jetbrains_ides[each.value].name
icon = local.icon icon = local.jetbrains_ides[each.value].icon
external = true external = true
order = var.order order = var.order
url = join("", [ url = join("", [
@@ -272,38 +326,45 @@ resource "coder_app" "gateway" {
"&token=", "&token=",
"$SESSION_TOKEN", "$SESSION_TOKEN",
"&ide_product_code=", "&ide_product_code=",
data.coder_parameter.jetbrains_ide.value, each.value,
"&ide_build_number=", "&ide_build_number=",
local.build_number, local.jetbrains_ides[each.value].build_number,
"&ide_download_link=", "&ide_download_link=",
local.download_link, local.jetbrains_ides[each.value].download_link,
]) ])
} }
output "identifier" { output "identifier" {
value = local.identifier value = local.identifier
description = "The product code of the JetBrains IDE."
} }
output "display_name" { output "display_name" {
value = local.display_name value = [for key in keys(coder_app.gateway) : coder_app.gateway[key].display_name]
description = "The display name of the JetBrains IDE."
} }
output "icon" { output "icon" {
value = local.icon value = [for key in keys(coder_app.gateway) : coder_app.gateway[key].icon]
description = "The icon of the JetBrains IDE."
} }
output "download_link" { output "download_link" {
value = local.download_link value = local.download_links
description = "The download link of the JetBrains IDE."
} }
output "build_number" { output "build_number" {
value = local.build_number value = local.build_numbers
description = "The build number of the JetBrains IDE."
} }
output "version" { output "version" {
value = local.version value = local.versions
description = "The version of the JetBrains IDE."
} }
output "url" { output "url" {
value = coder_app.gateway.url value = [for key in keys(coder_app.gateway) : coder_app.gateway[key].url]
} description = "The URL to connect to the JetBrains IDE."
}

View File

@@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template.
```tf ```tf
module "jupyterlab" { module "jupyterlab" {
source = "registry.coder.com/modules/jupyterlab/coder" source = "registry.coder.com/modules/jupyterlab/coder"
version = "1.0.22" version = "1.0.23"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/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 # Function to check if vncserver is already installed
check_installed() { check_installed() {
@@ -14,143 +15,167 @@ check_installed() {
# Function to download a file using wget, curl, or busybox as a fallback # Function to download a file using wget, curl, or busybox as a fallback
download_file() { download_file() {
local url=$1 local url="$1"
local output=$2 local output="$2"
if command -v wget &> /dev/null; then local download_tool
wget $url -O $output
elif command -v curl &> /dev/null; then if command -v curl &> /dev/null; then
curl -fsSL $url -o $output # 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 elif command -v busybox &> /dev/null; then
busybox wget -O $output $url # shellcheck disable=SC2034
download_tool=(busybox wget -O-)
else 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 exit 1
fi fi
# shellcheck disable=SC2288
"$${download_tool[@]}" "$url" > "$output" || {
echo "ERROR: Failed to download $url"
exit 1
}
} }
# Function to install kasmvncserver for debian-based distros # Function to install kasmvncserver for debian-based distros
install_deb() { install_deb() {
local url=$1 local url=$1
download_file $url /tmp/kasmvncserver.deb local kasmdeb="/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 download_file "$url" "$kasmdeb"
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 CACHE_DIR="/var/lib/apt/lists/partial"
install_rpm_centos7() { # Check if the directory exists and was modified in the last 60 minutes
local url=$1 if [[ ! -d "$CACHE_DIR" ]] || ! find "$CACHE_DIR" -mmin -60 -print -quit &> /dev/null; then
download_file $url /tmp/kasmvncserver.rpm echo "Stale package cache, updating..."
sudo yum install epel-release -y # Update package cache with a 300-second timeout for dpkg lock
sudo yum install /tmp/kasmvncserver.rpm -y sudo apt-get -o DPkg::Lock::Timeout=300 -qq update
sudo usermod -aG kasmvnc-cert $USER fi
rm /tmp/kasmvncserver.rpm
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 # Function to install kasmvncserver for rpm-based distros
install_rpm() { install_rpm() {
local url=$1 local url=$1
download_file $url /tmp/kasmvncserver.rpm local kasmrpm="/tmp/kasmvncserver.rpm"
sudo rpm -i /tmp/kasmvncserver.rpm local package_manager
rm /tmp/kasmvncserver.rpm
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 # Function to install kasmvncserver for Alpine Linux
install_alpine() { install_alpine() {
local url=$1 local url=$1
download_file $url /tmp/kasmvncserver.tgz local kasmtgz="/tmp/kasmvncserver.tgz"
tar -xzf /tmp/kasmvncserver.tgz -C /usr/local/bin/
rm /tmp/kasmvncserver.tgz download_file "$url" "$kasmtgz"
tar -xzf "$kasmtgz" -C /usr/local/bin/
rm "$kasmtgz"
} }
# Detect system information # Detect system information
distro=$(grep "^ID=" /etc/os-release | awk -F= '{print $2}') if [[ ! -f /etc/os-release ]]; then
version=$(grep "^VERSION_ID=" /etc/os-release | awk -F= '{print $2}' | tr -d '"') echo "ERROR: Cannot detect OS: /etc/os-release not found"
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 exit 1
fi 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: $distro_version"
echo "Detected Codename: $codename"
echo "Detected Architecture: $arch"
# Map arch to package arch
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 # Check if vncserver is installed, and install if not
if ! check_installed; then 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}"
echo "Installing KASM version: ${KASM_VERSION}"
case $distro in case $distro in
ubuntu | debian | kali) ubuntu | debian | kali)
case $version in bin_name="kasmvncserver_$${codename}_${KASM_VERSION}_$${arch}.deb"
"20.04") install_deb "$base_url/$bin_name"
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) oracle | fedora | opensuse)
if [[ "$version" == "8" ]]; then bin_name="kasmvncserver_$${distro}_$${distro_version}_${KASM_VERSION}_$${arch}.rpm"
install_rpm_oracle8 "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_oracle_8_${VERSION}_$${arch}.rpm" install_rpm "$base_url/$bin_name"
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) alpine)
if [[ "$version" == "3.17" || "$version" == "3.18" || "$version" == "3.19" || "$version" == "3.20" ]]; then bin_name="kasmvnc.alpine_$${distro_version//./}_$${arch}.tgz"
install_alpine "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvnc.alpine_$${version}_$${arch}.tgz" install_alpine "$base_url/$bin_name"
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}" echo "Unsupported distribution: $distro"
exit 1 exit 1
;; ;;
esac esac
@@ -158,22 +183,53 @@ else
echo "vncserver already installed. Skipping installation." echo "vncserver already installed. Skipping installation."
fi fi
# Coder port-forwarding from dashboard only supports HTTP if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
sudo bash -c "cat > /etc/kasmvnc/kasmvnc.yaml <<EOF 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: network:
protocol: http protocol: http
websocket_port: ${PORT} websocket_port: ${PORT}
ssl: ssl:
require_ssl: false require_ssl: false
pem_certificate:
pem_key:
udp: udp:
public_ip: 127.0.0.1 public_ip: 127.0.0.1
EOF" EOF
# This password is not used since we start the server without auth. # This password is not used since we start the server without auth.
# The server is protected via the Coder session token / tunnel # The server is protected via the Coder session token / tunnel
# and does not listen publicly # 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 # Start the server
printf "🚀 Starting KasmVNC server...\n" 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"

View File

@@ -200,7 +200,8 @@ export const runTerraformApply = async <TVars extends TerraformVariables>(
const combinedEnv = env === undefined ? {} : { ...env }; const combinedEnv = env === undefined ? {} : { ...env };
for (const [key, value] of Object.entries(vars)) { for (const [key, value] of Object.entries(vars)) {
combinedEnv[`TF_VAR_${key}`] = String(value); // Convert arrays to JSON strings
combinedEnv[`TF_VAR_${key}`] = Array.isArray(value) ? JSON.stringify(value) : String(value);
} }
const proc = spawn( const proc = spawn(