Compare commits

..

2 Commits

Author SHA1 Message Date
Ben Potter
cb075aa035 fix order 2024-06-26 16:44:21 -05:00
Ben Potter
9864408643 fix: aws starter template compatability 2024-06-26 16:34:52 -05:00
66 changed files with 329 additions and 1927 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +0,0 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -10,8 +10,6 @@ To create a new module, clone this repository and run:
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
Reference existing `*.test.ts` files for implementation. Reference existing `*.test.ts` files for implementation.
```shell ```shell

View File

@@ -1,23 +0,0 @@
---
display_name: airflow
description: A module that adds Apache Airflow in your Coder template
icon: ../.icons/airflow.svg
maintainer_github: coder
partner_github: nataindata
verified: true
tags: [airflow, idea, web, helper]
---
# airflow
A module that adds Apache Airflow in your Coder template.
```tf
module "airflow" {
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.13"
agent_id = coder_agent.main.id
}
```
![Airflow](../.images/airflow.png)

View File

@@ -1,65 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "log_path" {
type = string
description = "The path to log airflow to."
default = "/tmp/airflow.log"
}
variable "port" {
type = number
description = "The port to run airflow on."
default = 8080
}
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "airflow" {
agent_id = var.agent_id
display_name = "airflow"
icon = "/icon/apache-guacamole.svg"
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
PORT : var.port
})
run_on_start = true
}
resource "coder_app" "airflow" {
agent_id = var.agent_id
slug = "airflow"
display_name = "airflow"
url = "http://localhost:${var.port}"
icon = "/icon/apache-guacamole.svg"
subdomain = true
share = var.share
order = var.order
}

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env sh
BOLD='\033[0;1m'
PATH=$PATH:~/.local/bin
pip install --upgrade apache-airflow
filename=~/airflow/airflow.db
if ! [ -f $filename ] || ! [ -s $filename ]; then
airflow db init
fi
export AIRFLOW__CORE__LOAD_EXAMPLES=false
airflow webserver > ${LOG_PATH} 2>&1 &
airflow scheduler >> /tmp/airflow_scheduler.log 2>&1 &
airflow users create -u admin -p admin -r Admin -e admin@admin.com -f Coder -l User

View File

@@ -17,7 +17,7 @@ Customize the preselected parameter value:
```tf ```tf
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12" version = "1.0.10"
default = "us-east-1" default = "us-east-1"
} }
@@ -37,7 +37,7 @@ Change the display name and icon for a region using the corresponding maps:
```tf ```tf
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12" version = "1.0.10"
default = "ap-south-1" default = "ap-south-1"
custom_names = { custom_names = {
@@ -63,7 +63,7 @@ Hide the Asia Pacific regions Seoul and Osaka:
```tf ```tf
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "registry.coder.com/modules/aws-region/coder"
version = "1.0.12" version = "1.0.10"
exclude = ["ap-northeast-2", "ap-northeast-3"] exclude = ["ap-northeast-2", "ap-northeast-3"]
} }

View File

@@ -22,13 +22,4 @@ describe("aws-region", async () => {
}); });
expect(state.outputs.value.value).toBe("us-west-2"); expect(state.outputs.value.value).toBe("us-west-2");
}); });
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -51,12 +51,6 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# This is a static list because the regions don't change _that_ # This is a static list because the regions don't change _that_
# frequently and including the `aws_regions` data source requires # frequently and including the `aws_regions` data source requires
@@ -182,7 +176,6 @@ data "coder_parameter" "region" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
dynamic "option" { dynamic "option" {
for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) }

View File

@@ -14,7 +14,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele
```tf ```tf
module "azure_region" { module "azure_region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12" version = "1.0.2"
default = "eastus" default = "eastus"
} }
@@ -34,7 +34,7 @@ Change the display name and icon for a region using the corresponding maps:
```tf ```tf
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12" version = "1.0.2"
custom_names = { custom_names = {
"australia" : "Go Australia!" "australia" : "Go Australia!"
} }
@@ -57,7 +57,7 @@ Hide all regions in Australia except australiacentral:
```tf ```tf
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.12" version = "1.0.2"
exclude = [ exclude = [
"australia", "australia",
"australiacentral2", "australiacentral2",

View File

@@ -22,13 +22,4 @@ describe("azure-region", async () => {
}); });
expect(state.outputs.value.value).toBe("westus"); expect(state.outputs.value.value).toBe("westus");
}); });
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -50,12 +50,6 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# Note: Options are limited to 64 regions, some redundant regions have been removed. # Note: Options are limited to 64 regions, some redundant regions have been removed.
all_regions = { all_regions = {
@@ -315,7 +309,6 @@ data "coder_parameter" "region" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
icon = "/icon/azure.png" icon = "/icon/azure.png"
dynamic "option" { dynamic "option" {

View File

@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -28,7 +28,7 @@ module "code-server" {
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_version = "4.8.3" install_version = "4.8.3"
} }
@@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ extensions = [
"dracula-theme.theme-dracula" "dracula-theme.theme-dracula"
@@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"] extensions = ["dracula-theme.theme-dracula"]
settings = { settings = {
@@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
} }
@@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
use_cached = true use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf ```tf
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.16" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
offline = true offline = true
} }

View File

@@ -95,33 +95,6 @@ variable "use_cached" {
default = false default = false
} }
variable "use_cached_extensions" {
type = bool
description = "Uses cached copy of extensions, otherwise do a forced upgrade"
default = false
}
variable "extensions_dir" {
type = string
description = "Override the directory to store extensions in."
default = ""
}
variable "auto_install_extensions" {
type = bool
description = "Automatically install recommended extensions when code-server starts."
default = false
}
variable "subdomain" {
type = bool
description = <<-EOT
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
EOT
default = false
}
resource "coder_script" "code-server" { resource "coder_script" "code-server" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "code-server" display_name = "code-server"
@@ -137,10 +110,6 @@ resource "coder_script" "code-server" {
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline, OFFLINE : var.offline,
USE_CACHED : var.use_cached, USE_CACHED : var.use_cached,
USE_CACHED_EXTENSIONS : var.use_cached_extensions,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
}) })
run_on_start = true run_on_start = true
@@ -163,7 +132,7 @@ resource "coder_app" "code-server" {
display_name = var.display_name display_name = var.display_name
url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}" url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}"
icon = "/icon/code.svg" icon = "/icon/code.svg"
subdomain = var.subdomain subdomain = false
share = var.share share = var.share
order = var.order order = var.order

View File

@@ -6,16 +6,10 @@ CODE='\033[36;40;1m'
RESET='\033[0m' RESET='\033[0m'
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server" CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Set extension directory
EXTENSION_ARG=""
if [ -n "${EXTENSIONS_DIR}" ]; then
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
fi
function run_code_server() { function run_code_server() {
echo "👷 Running code-server in the background..." echo "👷 Running code-server in the background..."
echo "Check logs at ${LOG_PATH}!" echo "Check logs at ${LOG_PATH}!"
$CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 & $CODE_SERVER --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
} }
# Check if the settings file exists... # Check if the settings file exists...
@@ -25,53 +19,36 @@ if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
fi fi
# Check if code-server is already installed for offline # Check if code-server is already installed for offline or cached mode
if [ "${OFFLINE}" = true ]; then if [ -f "$CODE_SERVER" ]; then
if [ -f "$CODE_SERVER" ]; then if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
echo "🥳 Found a copy of code-server" echo "🥳 Found a copy of code-server"
run_code_server run_code_server
exit 0 exit 0
fi fi
# Offline mode always expects a copy of code-server to be present fi
# Offline mode always expects a copy of code-server to be present
if [ "${OFFLINE}" = true ]; then
echo "Failed to find a copy of code-server" echo "Failed to find a copy of code-server"
exit 1 exit 1
fi fi
# If there is no cached install OR we don't want to use a cached install printf "$${BOLD}Installing code-server!\n"
if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then
printf "$${BOLD}Installing code-server!\n"
ARGS=( ARGS=(
"--method=standalone" "--method=standalone"
"--prefix=${INSTALL_PREFIX}" "--prefix=${INSTALL_PREFIX}"
) )
if [ -n "${VERSION}" ]; then if [ -n "${VERSION}" ]; then
ARGS+=("--version=${VERSION}") ARGS+=("--version=${VERSION}")
fi
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
if [ $? -ne 0 ]; then
echo "Failed to install code-server: $output"
exit 1
fi
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
fi fi
# Get the list of installed extensions... output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG) if [ $? -ne 0 ]; then
readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS" echo "Failed to install code-server: $output"
function extension_installed() { exit 1
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then fi
return 1 printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
fi
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
if [ "$_extension" == "$1" ]; then
echo "Extension $1 was already installed."
return 0
fi
done
return 1
}
# Install each extension... # Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
@@ -79,38 +56,12 @@ for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then if [ -z "$extension" ]; then
continue continue
fi fi
if extension_installed "$extension"; then
continue
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension") output=$($CODE_SERVER --install-extension "$extension")
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output" echo "Failed to install extension: $extension: $output"
exit 1 exit 1
fi fi
done done
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
if ! command -v jq > /dev/null; then
echo "jq is required to install extensions from a workspace file."
exit 0
fi
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
for extension in $extensions; do
if extension_installed "$extension"; then
continue
fi
$CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension"
done
fi
fi
run_code_server run_code_server

View File

@@ -14,7 +14,7 @@ Automatically logs the user into Coder when creating their workspace.
```tf ```tf
module "coder-login" { module "coder-login" {
source = "registry.coder.com/modules/coder-login/coder" source = "registry.coder.com/modules/coder-login/coder"
version = "1.0.15" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.23" version = ">= 0.12"
} }
} }
} }
@@ -15,12 +15,11 @@ variable "agent_id" {
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "coder-login" { resource "coder_script" "coder-login" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
CODER_USER_TOKEN : data.coder_workspace_owner.me.session_token, CODER_USER_TOKEN : data.coder_workspace.me.owner_session_token,
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
}) })
display_name = "Coder Login" display_name = "Coder Login"

View File

@@ -9,70 +9,12 @@ tags: [helper]
# Dotfiles # Dotfiles
Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io). Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io)! Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
This will prompt the user for their dotfiles repository URL on template creation using a `coder_parameter`.
Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
```tf ```tf
module "dotfiles" { module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder" source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
## Examples
### Apply dotfiles as the current user
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
```
### Apply dotfiles as another user (only works if sudo is passwordless)
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
user = "root"
}
```
### Apply the same dotfiles as the current user and root (the root dotfiles can only be applied if sudo is passwordless)
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
}
```
## Setting a default dotfiles repository
You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable:
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
```

View File

@@ -18,23 +18,4 @@ describe("dotfiles", async () => {
}); });
expect(state.outputs.dotfiles_uri.value).toBe(""); expect(state.outputs.dotfiles_uri.value).toBe("");
}); });
it("set a default dotfiles_uri", async () => {
const default_dotfiles_uri = "foo";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
default_dotfiles_uri,
});
expect(state.outputs.dotfiles_uri.value).toBe(default_dotfiles_uri);
});
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(2);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -14,55 +14,24 @@ variable "agent_id" {
description = "The ID of a Coder agent." description = "The ID of a Coder agent."
} }
variable "default_dotfiles_uri" {
type = string
description = "The default dotfiles URI if the workspace user does not provide one"
default = ""
}
variable "dotfiles_uri" {
type = string
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
default = null
}
variable "user" {
type = string
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
default = null
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_parameter" "dotfiles_uri" { data "coder_parameter" "dotfiles_uri" {
count = var.dotfiles_uri == null ? 1 : 0
type = "string" type = "string"
name = "dotfiles_uri" name = "dotfiles_uri"
display_name = "Dotfiles URL" display_name = "Dotfiles URL (optional)"
order = var.coder_parameter_order default = ""
default = var.default_dotfiles_uri
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace" description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
mutable = true mutable = true
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
} }
locals { resource "coder_script" "personalize" {
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value agent_id = var.agent_id
user = var.user != null ? var.user : "" script = <<-EOT
} DOTFILES_URI="${data.coder_parameter.dotfiles_uri.value}"
if [ -n "$${DOTFILES_URI// }" ]; then
resource "coder_script" "dotfiles" { coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.dotfiles.log
agent_id = var.agent_id fi
script = templatefile("${path.module}/run.sh", { EOT
DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user
})
display_name = "Dotfiles" display_name = "Dotfiles"
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
run_on_start = true run_on_start = true
@@ -70,5 +39,5 @@ resource "coder_script" "dotfiles" {
output "dotfiles_uri" { output "dotfiles_uri" {
description = "Dotfiles URI" description = "Dotfiles URI"
value = local.dotfiles_uri value = data.coder_parameter.dotfiles_uri.value
} }

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
DOTFILES_URI="${DOTFILES_URI}"
DOTFILES_USER="${DOTFILES_USER}"
if [ -n "$${DOTFILES_URI// }" ]; then
if [ -z "$DOTFILES_USER" ]; then
DOTFILES_USER="$USER"
fi
echo "✨ Applying dotfiles for user $DOTFILES_USER"
if [ "$DOTFILES_USER" = "$USER" ]; then
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
else
# The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280
# eval echo ~coder -> "/home/coder"
# eval echo ~root -> "/root"
CODER_BIN=$(which coder)
DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER")
sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log"
fi
fi

View File

@@ -17,7 +17,7 @@ Customize the preselected parameter value:
```tf ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12" version = "1.0.2"
default = "standard.medium" default = "standard.medium"
} }
@@ -45,7 +45,7 @@ Change the display name a type using the corresponding maps:
```tf ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12" version = "1.0.2"
default = "standard.medium" default = "standard.medium"
custom_names = { custom_names = {
@@ -79,7 +79,7 @@ Show only gpu1 types
```tf ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.12" version = "1.0.2"
default = "gpu.large" default = "gpu.large"
type_category = ["gpu"] type_category = ["gpu"]
exclude = [ exclude = [

View File

@@ -31,13 +31,4 @@ describe("exoscale-instance-type", async () => {
}); });
}).toThrow('default value "gpu3.huge" must be defined as one of options'); }).toThrow('default value "gpu3.huge" must be defined as one of options');
}); });
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -56,12 +56,6 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# https://www.exoscale.com/pricing/ # https://www.exoscale.com/pricing/
@@ -263,7 +257,6 @@ data "coder_parameter" "instance_type" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
dynamic "option" { dynamic "option" {
for_each = [for k, v in concat( for_each = [for k, v in concat(

View File

@@ -17,7 +17,7 @@ Customize the preselected parameter value:
```tf ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.12" version = "1.0.2"
default = "ch-dk-2" default = "ch-dk-2"
} }
@@ -44,7 +44,7 @@ Change the display name and icon for a zone using the corresponding maps:
```tf ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.12" version = "1.0.2"
default = "at-vie-1" default = "at-vie-1"
custom_names = { custom_names = {
@@ -76,7 +76,7 @@ Hide the Switzerland zones Geneva and Zurich
```tf ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.12" version = "1.0.2"
exclude = ["ch-gva-2", "ch-dk-2"] exclude = ["ch-gva-2", "ch-dk-2"]
} }

View File

@@ -22,13 +22,4 @@ describe("exoscale-zone", async () => {
}); });
expect(state.outputs.value.value).toBe("at-vie-1"); expect(state.outputs.value.value).toBe("at-vie-1");
}); });
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -51,11 +51,6 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# This is a static list because the zones don't change _that_ # This is a static list because the zones don't change _that_
@@ -99,7 +94,6 @@ data "coder_parameter" "zone" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
dynamic "option" { dynamic "option" {
for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) } for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) }

View File

@@ -14,7 +14,7 @@ This module adds Google Cloud Platform regions to your Coder template.
```tf ```tf
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12" version = "1.0.2"
regions = ["us", "europe"] regions = ["us", "europe"]
} }
@@ -34,7 +34,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support,
```tf ```tf
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12" version = "1.0.2"
default = ["us-west1-a"] default = ["us-west1-a"]
regions = ["us-west1"] regions = ["us-west1"]
gpu_only = false gpu_only = false
@@ -50,7 +50,7 @@ resource "google_compute_instance" "example" {
```tf ```tf
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12" version = "1.0.2"
regions = ["europe-west"] regions = ["europe-west"]
single_zone_per_region = false single_zone_per_region = false
} }
@@ -65,7 +65,7 @@ resource "google_compute_instance" "example" {
```tf ```tf
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "registry.coder.com/modules/gcp-region/coder"
version = "1.0.12" version = "1.0.2"
regions = ["us", "europe"] regions = ["us", "europe"]
gpu_only = true gpu_only = true
single_zone_per_region = true single_zone_per_region = true

View File

@@ -40,13 +40,4 @@ describe("gcp-region", async () => {
}); });
expect(state.outputs.value.value).toBe("us-west2-b"); expect(state.outputs.value.value).toBe("us-west2-b");
}); });
it("set custom order for coder_parameter", async () => {
const order = 99;
const state = await runTerraformApply(import.meta.dir, {
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(1);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
});
}); });

View File

@@ -63,12 +63,6 @@ variable "single_zone_per_region" {
type = bool type = bool
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
zones = { zones = {
# US Central # US Central
@@ -721,7 +715,6 @@ data "coder_parameter" "region" {
icon = "/icon/gcp.png" icon = "/icon/gcp.png"
mutable = var.mutable mutable = var.mutable
default = var.default != null && var.default != "" && (!var.gpu_only || try(local.zones[var.default].gpu, false)) ? var.default : null default = var.default != null && var.default != "" && (!var.gpu_only || try(local.zones[var.default].gpu, false)) ? var.default : null
order = var.coder_parameter_order
dynamic "option" { dynamic "option" {
for_each = { for_each = {
for k, v in local.zones : k => v for k, v in local.zones : k => v

View File

@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
```tf ```tf
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
url = "https://github.com/coder/coder" url = "https://github.com/coder/coder"
} }
@@ -27,7 +27,7 @@ module "git-clone" {
```tf ```tf
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
url = "https://github.com/coder/coder" url = "https://github.com/coder/coder"
base_dir = "~/projects/coder" base_dir = "~/projects/coder"
@@ -41,7 +41,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
```tf ```tf
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
url = "https://github.com/coder/coder" url = "https://github.com/coder/coder"
} }
@@ -50,106 +50,3 @@ data "coder_git_auth" "github" {
id = "github" id = "github"
} }
``` ```
## GitHub clone with branch name
To GitHub clone with a specific branch like `feat/example`
```tf
# Prompt the user for the git repo URL
data "coder_parameter" "git_repo" {
name = "git_repo"
display_name = "Git repository"
default = "https://github.com/coder/coder/tree/feat/example"
}
# Clone the repository for branch `feat/example`
module "git_clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
# Create a code-server instance for the cloned repository
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone.folder_name}"
}
# Create a Coder app for the website
resource "coder_app" "website" {
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
}
```
Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
## GitLab clone with branch name
To GitLab clone with a specific branch like `feat/example`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
```
Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
## Git clone with branch_name set
Alternatively, you can set the `branch_name` attribute to clone a specific branch.
For example, to clone the `feat/example` branch:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
}
```

View File

@@ -36,196 +36,4 @@ describe("git-clone", async () => {
"Cloning fake-url to ~/fake-url...", "Cloning fake-url to ~/fake-url...",
]); ]);
}); });
it("repo_dir should match repo name for https", async () => {
const url = "https://github.com/coder/coder.git";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url,
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
expect(state.outputs.folder_name.value).toEqual("coder");
expect(state.outputs.clone_url.value).toEqual(url);
expect(state.outputs.web_url.value).toEqual(url);
expect(state.outputs.branch_name.value).toEqual("");
});
it("repo_dir should match repo name for https without .git", async () => {
const url = "https://github.com/coder/coder";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url,
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
expect(state.outputs.clone_url.value).toEqual(url);
expect(state.outputs.web_url.value).toEqual(url);
expect(state.outputs.branch_name.value).toEqual("");
});
it("repo_dir should match repo name for ssh", async () => {
const url = "git@github.com:coder/coder.git";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url,
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
expect(state.outputs.git_provider.value).toEqual("");
expect(state.outputs.clone_url.value).toEqual(url);
const https_url = "https://github.com/coder/coder.git";
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("");
});
it("branch_name should not include query string", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch?ref_type=heads",
});
expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log");
expect(state.outputs.folder_name.value).toEqual("repo-tests.log");
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
expect(state.outputs.clone_url.value).toEqual(https_url);
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("feat/branch");
});
it("branch_name should not include fragments", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch#name",
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
expect(state.outputs.clone_url.value).toEqual(https_url);
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("feat/branch");
});
it("gitlab url with branch should match", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch",
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
expect(state.outputs.git_provider.value).toEqual("gitlab");
const https_url = "https://gitlab.com/mike.brew/repo-tests.log";
expect(state.outputs.clone_url.value).toEqual(https_url);
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("feat/branch");
});
it("github url with branch should match", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch",
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/repo-tests.log");
expect(state.outputs.git_provider.value).toEqual("github");
const https_url = "https://github.com/michaelbrewer/repo-tests.log";
expect(state.outputs.clone_url.value).toEqual(https_url);
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("feat/branch");
});
it("self-host git url with branch should match", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url: "https://git.example.com/example/project/-/tree/feat/example",
git_providers: `
{
"https://git.example.com/" = {
provider = "gitlab"
}
}`,
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/project");
expect(state.outputs.git_provider.value).toEqual("gitlab");
const https_url = "https://git.example.com/example/project";
expect(state.outputs.clone_url.value).toEqual(https_url);
expect(state.outputs.web_url.value).toEqual(https_url);
expect(state.outputs.branch_name.value).toEqual("feat/example");
});
it("handle unsupported git provider configuration", async () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "foo",
git_providers: `
{
"https://git.example.com/" = {
provider = "bitbucket"
}
}`,
});
};
expect(t).toThrow('Allowed values for provider are "github" or "gitlab".');
});
it("handle unknown git provider url", async () => {
const url = "https://git.unknown.com/coder/coder";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
base_dir: "/tmp",
url,
});
expect(state.outputs.repo_dir.value).toEqual("/tmp/coder");
expect(state.outputs.clone_url.value).toEqual(url);
expect(state.outputs.web_url.value).toEqual(url);
expect(state.outputs.branch_name.value).toEqual("");
});
it("runs with github clone with switch to feat/branch", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "https://github.com/michaelbrewer/repo-tests.log/tree/feat/branch",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
it("runs with gitlab clone with switch to feat/branch", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url: "https://gitlab.com/mike.brew/repo-tests.log/-/tree/feat/branch",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://gitlab.com/mike.brew/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
it("runs with github clone with branch_name set to feat/branch", async () => {
const url = "https://github.com/michaelbrewer/repo-tests.log";
const branch_name = "feat/branch";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
url,
branch_name,
});
expect(state.outputs.repo_dir.value).toEqual("~/repo-tests.log");
expect(state.outputs.clone_url.value).toEqual(url);
expect(state.outputs.web_url.value).toEqual(url);
expect(state.outputs.branch_name.value).toEqual(branch_name);
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"Creating directory ~/repo-tests.log...",
"Cloning https://github.com/michaelbrewer/repo-tests.log to ~/repo-tests.log on branch feat/branch...",
]);
});
}); });

View File

@@ -25,50 +25,8 @@ variable "agent_id" {
type = string type = string
} }
variable "git_providers" {
type = map(object({
provider = string
}))
description = "A mapping of URLs to their git provider."
default = {
"https://github.com/" = {
provider = "github"
},
"https://gitlab.com/" = {
provider = "gitlab"
},
}
validation {
error_message = "Allowed values for provider are \"github\" or \"gitlab\"."
condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)])
}
}
variable "branch_name" {
description = "The branch name to clone. If not provided, the default branch will be cloned."
type = string
default = ""
}
locals { locals {
# Remove query parameters and fragments from the URL clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")])
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
# Find the git provider based on the URL and determine the tree path
provider_key = try(one([for key in keys(var.git_providers) : key if startswith(local.url, key)]), null)
provider = try(lookup(var.git_providers, local.provider_key).provider, "")
tree_path = local.provider == "gitlab" ? "/-/tree/" : local.provider == "github" ? "/tree/" : ""
# Remove tree and branch name from the URL
clone_url = var.branch_name == "" && local.tree_path != "" ? replace(local.url, "/${local.tree_path}.*/", "") : local.url
# Extract the branch name from the URL
branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name
# Extract the folder name from the URL
folder_name = replace(basename(local.clone_url), ".git", "")
# Construct the path to clone the repository
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
# Construct the web URL
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
} }
output "repo_dir" { output "repo_dir" {
@@ -76,37 +34,11 @@ output "repo_dir" {
description = "Full path of cloned repo directory" description = "Full path of cloned repo directory"
} }
output "git_provider" {
value = local.provider
description = "The git provider of the repository"
}
output "folder_name" {
value = local.folder_name
description = "The name of the folder that will be created"
}
output "clone_url" {
value = local.clone_url
description = "The exact Git repository URL that will be cloned"
}
output "web_url" {
value = local.web_url
description = "Git https repository URL (may be invalid for unsupported providers)"
}
output "branch_name" {
value = local.branch_name
description = "Git branch name (may be empty)"
}
resource "coder_script" "git_clone" { resource "coder_script" "git_clone" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
CLONE_PATH = local.clone_path, CLONE_PATH = local.clone_path
REPO_URL : local.clone_url, REPO_URL : var.url,
BRANCH_NAME : local.branch_name,
}) })
display_name = "Git Clone" display_name = "Git Clone"
icon = "/icon/git.svg" icon = "/icon/git.svg"

View File

@@ -2,7 +2,6 @@
REPO_URL="${REPO_URL}" REPO_URL="${REPO_URL}"
CLONE_PATH="${CLONE_PATH}" CLONE_PATH="${CLONE_PATH}"
BRANCH_NAME="${BRANCH_NAME}"
# Expand home if it's specified! # Expand home if it's specified!
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}" CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
@@ -34,13 +33,8 @@ fi
# Check if the directory is empty # Check if the directory is empty
# and if it is, clone the repo, otherwise skip cloning # and if it is, clone the repo, otherwise skip cloning
if [ -z "$(ls -A "$CLONE_PATH")" ]; then if [ -z "$(ls -A "$CLONE_PATH")" ]; then
if [ -z "$BRANCH_NAME" ]; then echo "Cloning $REPO_URL to $CLONE_PATH..."
echo "Cloning $REPO_URL to $CLONE_PATH..." git clone "$REPO_URL" "$CLONE_PATH"
git clone "$REPO_URL" "$CLONE_PATH"
else
echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..."
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
fi
else else
echo "$CLONE_PATH already exists and isn't empty, skipping clone!" echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
exit 0 exit 0

View File

@@ -19,7 +19,7 @@ This module has a chance of conflicting with the user's dotfiles / the personali
```tf ```tf
module "git-commit-signing" { module "git-commit-signing" {
source = "registry.coder.com/modules/git-commit-signing/coder" source = "registry.coder.com/modules/git-commit-signing/coder"
version = "1.0.11" version = "1.0.9"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -14,7 +14,7 @@ Runs a script that updates git credentials in the workspace to match the user's
```tf ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15" version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -28,7 +28,7 @@ TODO: Add screenshot
```tf ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15" version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
allow_email_change = true allow_email_change = true
} }
@@ -41,7 +41,7 @@ TODO: Add screenshot
```tf ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.15" version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
allow_username_change = false allow_username_change = false
allow_email_change = false allow_email_change = false

View File

@@ -1,127 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("git-config", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("can run apply allow_username_change and allow_email_change disabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_username_change: "false",
allow_email_change: "false",
});
const resources = state.resources;
expect(resources).toHaveLength(6);
expect(resources).toMatchObject([
{ type: "coder_workspace", name: "me" },
{ type: "coder_workspace_owner", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
});
it("can run apply allow_email_change enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_email_change: "true",
});
const resources = state.resources;
expect(resources).toHaveLength(8);
expect(resources).toMatchObject([
{ type: "coder_parameter", name: "user_email" },
{ type: "coder_parameter", name: "username" },
{ type: "coder_workspace", name: "me" },
{ type: "coder_workspace_owner", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
});
it("can run apply allow_email_change enabled", async () => {
const state = await runTerraformApply(
import.meta.dir,
{
agent_id: "foo",
allow_username_change: "false",
allow_email_change: "false",
},
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@email.com" },
);
const resources = state.resources;
expect(resources).toHaveLength(6);
expect(resources).toMatchObject([
{ type: "coder_workspace", name: "me" },
{ type: "coder_workspace_owner", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
});
it("set custom order for coder_parameter for both fields", async () => {
const order = 20;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_username_change: "true",
allow_email_change: "true",
coder_parameter_order: order.toString(),
});
const resources = state.resources;
expect(resources).toHaveLength(8);
expect(resources).toMatchObject([
{ type: "coder_parameter", name: "user_email" },
{ type: "coder_parameter", name: "username" },
{ type: "coder_workspace", name: "me" },
{ type: "coder_workspace_owner", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
// user_email order is the same as the order
expect(resources[0].instances[0].attributes.order).toBe(order);
// username order is incremented by 1
// @ts-ignore: Object is possibly 'null'.
expect(resources[1].instances[0]?.attributes.order).toBe(order + 1);
});
it("set custom order for coder_parameter for just username", async () => {
const order = 30;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_email_change: "false",
allow_username_change: "true",
coder_parameter_order: order.toString(),
});
const resources = state.resources;
expect(resources).toHaveLength(7);
expect(resources).toMatchObject([
{ type: "coder_parameter", name: "username" },
{ type: "coder_workspace", name: "me" },
{ type: "coder_workspace_owner", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
// user_email was not created
// username order is incremented by 1
expect(resources[0].instances[0].attributes.order).toBe(order + 1);
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.23" version = ">= 0.13"
} }
} }
} }
@@ -26,21 +26,14 @@ variable "allow_email_change" {
default = false default = false
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "user_email" { data "coder_parameter" "user_email" {
count = var.allow_email_change ? 1 : 0 count = var.allow_email_change ? 1 : 0
name = "user_email" name = "user_email"
type = "string" type = "string"
default = "" default = ""
order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null
description = "Git user.email to be used for commits. Leave empty to default to Coder user's email." description = "Git user.email to be used for commits. Leave empty to default to Coder user's email."
display_name = "Git config user.email" display_name = "Git config user.email"
mutable = true mutable = true
@@ -51,7 +44,6 @@ data "coder_parameter" "username" {
name = "username" name = "username"
type = "string" type = "string"
default = "" default = ""
order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null
description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name." description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
display_name = "Full Name for Git config" display_name = "Full Name for Git config"
mutable = true mutable = true
@@ -60,25 +52,23 @@ data "coder_parameter" "username" {
resource "coder_env" "git_author_name" { resource "coder_env" "git_author_name" {
agent_id = var.agent_id agent_id = var.agent_id
name = "GIT_AUTHOR_NAME" name = "GIT_AUTHOR_NAME"
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
} }
resource "coder_env" "git_commmiter_name" { resource "coder_env" "git_commmiter_name" {
agent_id = var.agent_id agent_id = var.agent_id
name = "GIT_COMMITTER_NAME" name = "GIT_COMMITTER_NAME"
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
} }
resource "coder_env" "git_author_email" { resource "coder_env" "git_author_email" {
agent_id = var.agent_id agent_id = var.agent_id
name = "GIT_AUTHOR_EMAIL" name = "GIT_AUTHOR_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email) value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
} }
resource "coder_env" "git_commmiter_email" { resource "coder_env" "git_commmiter_email" {
agent_id = var.agent_id agent_id = var.agent_id
name = "GIT_COMMITTER_EMAIL" name = "GIT_COMMITTER_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email) value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
} }

View File

@@ -1,53 +0,0 @@
---
display_name: Github Upload Public Key
description: Automates uploading Coder public key to Github so users don't have to.
icon: ../.icons/github.svg
maintainer_github: coder
verified: true
tags: [helper, git]
---
# github-upload-public-key
Templates that utilize Github External Auth can automatically ensure that the Coder public key is uploaded to Github so that users can clone repositories without needing to upload the public key themselves.
```tf
module "github-upload-public-key" {
source = "registry.coder.com/modules/github-upload-public-key/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
}
```
# Requirements
This module requires `curl` and `jq` to be installed inside your workspace.
Github External Auth must be enabled in the workspace for this module to work. The Github app that is configured for external auth must have both read and write permissions to "Git SSH keys" in order to upload the public key. Additionally, a Coder admin must also have the `admin:public_key` scope added to the external auth configuration of the Coder deployment. For example:
```
CODER_EXTERNAL_AUTH_0_ID="USER_DEFINED_ID"
CODER_EXTERNAL_AUTH_0_TYPE=github
CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx
CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx
CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key"
```
Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes.
# Example
Using a coder github external auth with a non-default id: (default is `github`)
```tf
data "coder_external_auth" "github" {
id = "myauthid"
}
module "github-upload-public-key" {
source = "registry.coder.com/modules/github-upload-public-key/coder"
version = "1.0.15"
agent_id = coder_agent.example.id
external_auth_id = data.coder_external_auth.github.id
}
```

View File

@@ -1,128 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
createJSONResponse,
execContainer,
findResourceInstance,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
writeCoder,
} from "../test";
import { Server, serve } from "bun";
describe("github-upload-public-key", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("creates new key if one does not exist", async () => {
const { instance, id, server } = await setupContainer();
await writeCoder(id, "echo foo");
let exec = await execContainer(id, [
"env",
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key has been added to GitHub!",
);
expect(exec.exitCode).toBe(0);
// we need to increase timeout to pull the container
}, 15000);
it("does nothing if one already exists", async () => {
const { instance, id, server } = await setupContainer();
// use keyword to make server return a existing key
await writeCoder(id, "echo findkey");
let exec = await execContainer(id, [
"env",
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
"CODER_OWNER_SESSION_TOKEN=foo",
"CODER_EXTERNAL_AUTH_ID=github",
"bash",
"-c",
instance.script,
]);
expect(exec.stdout).toContain(
"Your Coder public key is already on GitHub!",
);
expect(exec.exitCode).toBe(0);
});
});
const setupContainer = async (
image = "lorello/alpine-bash",
vars: Record<string, string> = {},
) => {
const server = await setupServer();
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...vars,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer(image);
return { id, instance, server };
};
const setupServer = async (): Promise<Server> => {
let url: URL;
const fakeSlackHost = serve({
fetch: (req) => {
url = new URL(req.url);
if (url.pathname === "/api/v2/users/me/gitsshkey") {
return createJSONResponse({
public_key: "exists",
});
}
if (url.pathname === "/user/keys") {
if (req.method === "POST") {
return createJSONResponse(
{
key: "created",
},
201,
);
}
// case: key already exists
if (req.headers.get("Authorization") == "Bearer findkey") {
return createJSONResponse([
{
key: "foo",
},
{
key: "exists",
},
]);
}
// case: key does not exist
return createJSONResponse([
{
key: "foo",
},
]);
}
return createJSONResponse(
{
error: "not_found",
},
404,
);
},
port: 0,
});
return fakeSlackHost;
};

View File

@@ -1,43 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.23"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "external_auth_id" {
type = string
description = "The ID of the GitHub external auth."
default = "github"
}
variable "github_api_url" {
type = string
description = "The URL of the GitHub instance."
default = "https://api.github.com"
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "github_upload_public_key" {
agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", {
CODER_OWNER_SESSION_TOKEN : data.coder_workspace_owner.me.session_token,
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
GITHUB_API_URL : var.github_api_url,
})
display_name = "Github Upload Public Key"
icon = "/icon/github.svg"
run_on_start = true
}

View File

@@ -1,110 +0,0 @@
#!/usr/bin/env bash
if [ -z "$CODER_ACCESS_URL" ]; then
if [ -z "${CODER_ACCESS_URL}" ]; then
echo "CODER_ACCESS_URL is empty!"
exit 1
fi
CODER_ACCESS_URL=${CODER_ACCESS_URL}
fi
if [ -z "$CODER_OWNER_SESSION_TOKEN" ]; then
if [ -z "${CODER_OWNER_SESSION_TOKEN}" ]; then
echo "CODER_OWNER_SESSION_TOKEN is empty!"
exit 1
fi
CODER_OWNER_SESSION_TOKEN=${CODER_OWNER_SESSION_TOKEN}
fi
if [ -z "$CODER_EXTERNAL_AUTH_ID" ]; then
if [ -z "${CODER_EXTERNAL_AUTH_ID}" ]; then
echo "CODER_EXTERNAL_AUTH_ID is empty!"
exit 1
fi
CODER_EXTERNAL_AUTH_ID=${CODER_EXTERNAL_AUTH_ID}
fi
if [ -z "$GITHUB_API_URL" ]; then
if [ -z "${GITHUB_API_URL}" ]; then
echo "GITHUB_API_URL is empty!"
exit 1
fi
GITHUB_API_URL=${GITHUB_API_URL}
fi
echo "Fetching GitHub token..."
GITHUB_TOKEN=$(coder external-auth access-token $CODER_EXTERNAL_AUTH_ID)
if [ $? -ne 0 ]; then
printf "Authenticate with Github to automatically upload Coder public key:\n$GITHUB_TOKEN\n"
exit 1
fi
echo "Fetching public key from Coder..."
PUBLIC_KEY_RESPONSE=$(
curl -L -s \
-w "\n%%{http_code}" \
-H 'accept: application/json' \
-H "cookie: coder_session_token=$CODER_OWNER_SESSION_TOKEN" \
"$CODER_ACCESS_URL/api/v2/users/me/gitsshkey"
)
PUBLIC_KEY_RESPONSE_STATUS=$(tail -n1 <<< "$PUBLIC_KEY_RESPONSE")
PUBLIC_KEY_BODY=$(sed \$d <<< "$PUBLIC_KEY_RESPONSE")
if [ "$PUBLIC_KEY_RESPONSE_STATUS" -ne 200 ]; then
echo "Failed to fetch Coder public SSH key with status code $PUBLIC_KEY_RESPONSE_STATUS!"
echo "$PUBLIC_KEY_BODY"
exit 1
fi
PUBLIC_KEY=$(jq -r '.public_key' <<< "$PUBLIC_KEY_BODY")
if [ -z "$PUBLIC_KEY" ]; then
echo "No Coder public SSH key found!"
exit 1
fi
echo "Fetching public keys from GitHub..."
GITHUB_KEYS_RESPONSE=$(
curl -L -s \
-w "\n%%{http_code}" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
$GITHUB_API_URL/user/keys
)
GITHUB_KEYS_RESPONSE_STATUS=$(tail -n1 <<< "$GITHUB_KEYS_RESPONSE")
GITHUB_KEYS_RESPONSE_BODY=$(sed \$d <<< "$GITHUB_KEYS_RESPONSE")
if [ "$GITHUB_KEYS_RESPONSE_STATUS" -ne 200 ]; then
echo "Failed to fetch Coder public SSH key with status code $GITHUB_KEYS_RESPONSE_STATUS!"
echo "$GITHUB_KEYS_RESPONSE_BODY"
exit 1
fi
GITHUB_MATCH=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$GITHUB_KEYS_RESPONSE_BODY")
if [ "$PUBLIC_KEY" = "$GITHUB_MATCH" ]; then
echo "Your Coder public key is already on GitHub!"
exit 0
fi
echo "Your Coder public key is not in GitHub. Adding it now..."
CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces"
UPLOAD_RESPONSE=$(
curl -L -s \
-X POST \
-w "\n%%{http_code}" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2022-11-28" \
$GITHUB_API_URL/user/keys \
-d "{\"title\":\"$CODER_PUBLIC_KEY_NAME\",\"key\":\"$PUBLIC_KEY\"}"
)
UPLOAD_RESPONSE_STATUS=$(tail -n1 <<< "$UPLOAD_RESPONSE")
UPLOAD_RESPONSE_BODY=$(sed \$d <<< "$UPLOAD_RESPONSE")
if [ "$UPLOAD_RESPONSE_STATUS" -ne 201 ]; then
echo "Failed to upload Coder public SSH key with status code $UPLOAD_RESPONSE_STATUS!"
echo "$UPLOAD_RESPONSE_BODY"
exit 1
fi
echo "Your Coder public key has been added to GitHub!"

View File

@@ -14,7 +14,7 @@ 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.13" version = "1.0.9"
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"
@@ -27,12 +27,12 @@ module "jetbrains_gateway" {
## Examples ## Examples
### Add GoLand and WebStorm as options with the default set to GoLand ### Add GoLand and WebStorm with the default set to GoLand
```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.13" version = "1.0.9"
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"
@@ -41,37 +41,6 @@ module "jetbrains_gateway" {
} }
``` ```
### Use the latest release version
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.13"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
latest = true
}
```
### Use the latest EAP version
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.13"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
latest = true
channel = "eap"
}
```
## 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

@@ -6,10 +6,6 @@ terraform {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.17"
} }
http = {
source = "hashicorp/http"
version = ">= 3.0"
}
} }
} }
@@ -50,22 +46,6 @@ variable "coder_parameter_order" {
default = null default = null
} }
variable "latest" {
type = bool
description = "Whether to fetch the latest version of the IDE."
default = false
}
variable "channel" {
type = string
description = "JetBrains IDE release channel. Valid values are release and eap."
default = "release"
validation {
condition = can(regex("^(release|eap)$", var.channel))
error_message = "The channel must be either release or eap."
}
}
variable "jetbrains_ide_versions" { variable "jetbrains_ide_versions" {
type = map(object({ type = map(object({
build_number = string build_number = string
@@ -74,36 +54,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 = "233.14808.21"
version = "2024.1" version = "2023.3.5"
} }
"PS" = { "PS" = {
build_number = "241.14494.237" build_number = "233.14808.18"
version = "2024.1" version = "2023.3.5"
} }
"WS" = { "WS" = {
build_number = "241.14494.235" build_number = "233.14475.40"
version = "2024.1" version = "2023.3.4"
} }
"PY" = { "PY" = {
build_number = "241.14494.241" build_number = "233.14475.56"
version = "2024.1" version = "2023.3.4"
} }
"CL" = { "CL" = {
build_number = "241.14494.288" build_number = "233.14475.31"
version = "2024.1" version = "2023.3.4"
} }
"GO" = { "GO" = {
build_number = "241.14494.238" build_number = "233.14808.20"
version = "2024.1" version = "2023.3.5"
} }
"RM" = { "RM" = {
build_number = "241.14494.234" build_number = "233.14808.14"
version = "2024.1" version = "2023.3.5"
} }
"RD" = { "RD" = {
build_number = "241.14494.307" build_number = "233.14475.66"
version = "2024.1" version = "2023.3.4"
} }
} }
validation { validation {
@@ -140,11 +120,6 @@ variable "jetbrains_ides" {
} }
} }
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}"
}
locals { locals {
jetbrains_ides = { jetbrains_ides = {
"GO" = { "GO" = {
@@ -153,7 +128,6 @@ locals {
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 = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
version = var.jetbrains_ide_versions["GO"].version
}, },
"WS" = { "WS" = {
icon = "/icon/webstorm.svg", icon = "/icon/webstorm.svg",
@@ -161,7 +135,6 @@ locals {
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 = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
version = var.jetbrains_ide_versions["WS"].version
}, },
"IU" = { "IU" = {
icon = "/icon/intellij.svg", icon = "/icon/intellij.svg",
@@ -169,7 +142,6 @@ locals {
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 = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
version = var.jetbrains_ide_versions["IU"].version
}, },
"PY" = { "PY" = {
icon = "/icon/pycharm.svg", icon = "/icon/pycharm.svg",
@@ -177,7 +149,6 @@ locals {
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 = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
version = var.jetbrains_ide_versions["PY"].version
}, },
"CL" = { "CL" = {
icon = "/icon/clion.svg", icon = "/icon/clion.svg",
@@ -185,7 +156,6 @@ locals {
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 = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
version = var.jetbrains_ide_versions["CL"].version
}, },
"PS" = { "PS" = {
icon = "/icon/phpstorm.svg", icon = "/icon/phpstorm.svg",
@@ -193,7 +163,6 @@ locals {
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 = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
version = var.jetbrains_ide_versions["PS"].version
}, },
"RM" = { "RM" = {
icon = "/icon/rubymine.svg", icon = "/icon/rubymine.svg",
@@ -201,7 +170,6 @@ locals {
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 = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
version = var.jetbrains_ide_versions["RM"].version
} }
"RD" = { "RD" = {
icon = "/icon/rider.svg", icon = "/icon/rider.svg",
@@ -209,18 +177,8 @@ locals {
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 = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
version = var.jetbrains_ide_versions["RD"].version
} }
} }
icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon
json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {}
key = var.latest ? keys(local.json_data)[0] : ""
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
identifier = data.coder_parameter.jetbrains_ide.value
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
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
} }
data "coder_parameter" "jetbrains_ide" { data "coder_parameter" "jetbrains_ide" {
@@ -235,9 +193,9 @@ data "coder_parameter" "jetbrains_ide" {
dynamic "option" { dynamic "option" {
for_each = var.jetbrains_ides for_each = var.jetbrains_ides
content { content {
icon = local.jetbrains_ides[option.value].icon icon = lookup(local.jetbrains_ides, option.value).icon
name = local.jetbrains_ides[option.value].name name = lookup(local.jetbrains_ides, option.value).name
value = option.value value = lookup(local.jetbrains_ides, option.value).identifier
} }
} }
} }
@@ -247,8 +205,8 @@ data "coder_workspace" "me" {}
resource "coder_app" "gateway" { resource "coder_app" "gateway" {
agent_id = var.agent_id agent_id = var.agent_id
slug = "gateway" slug = "gateway"
display_name = local.display_name display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE")
icon = local.icon icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg")
external = true external = true
order = var.order order = var.order
url = join("", [ url = join("", [
@@ -263,36 +221,36 @@ resource "coder_app" "gateway" {
"&token=", "&token=",
"$SESSION_TOKEN", "$SESSION_TOKEN",
"&ide_product_code=", "&ide_product_code=",
data.coder_parameter.jetbrains_ide.value, local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier,
"&ide_build_number=", "&ide_build_number=",
local.build_number, local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number,
"&ide_download_link=", "&ide_download_link=",
local.download_link, local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
]) ])
} }
output "identifier" { output "identifier" {
value = local.identifier value = data.coder_parameter.jetbrains_ide.value
} }
output "display_name" { output "name" {
value = local.display_name value = coder_app.gateway.display_name
} }
output "icon" { output "icon" {
value = local.icon value = coder_app.gateway.icon
} }
output "download_link" { output "download_link" {
value = local.download_link value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link
} }
output "build_number" { output "build_number" {
value = local.build_number value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number
} }
output "version" { output "version" {
value = local.version value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
} }
output "url" { output "url" {

View File

@@ -17,7 +17,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.15" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -44,7 +44,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.15" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "email" username_field = "email"
@@ -72,7 +72,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.15" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.23" version = ">= 0.12.4"
} }
} }
} }
@@ -68,12 +68,11 @@ EOF
locals { locals {
# The username field to use for artifactory # The username field to use for artifactory
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
jfrog_host = replace(var.jfrog_url, "https://", "") jfrog_host = replace(var.jfrog_url, "https://", "")
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_external_auth" "jfrog" { data "coder_external_auth" "jfrog" {
id = var.external_auth_id id = var.external_auth_id
@@ -88,7 +87,7 @@ resource "coder_script" "jfrog" {
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id, JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email, ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token, ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server, CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""), REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),

View File

@@ -15,7 +15,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.15" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
@@ -41,7 +41,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.15" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io" jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token artifactory_access_token = var.artifactory_access_token # An admin access token
@@ -74,7 +74,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.15" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
@@ -94,11 +94,11 @@ data "coder_workspace" "me" {}
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.15" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}" token_description = "Token for Coder workspace: ${data.coder_workspace.me.owner}/${data.coder_workspace.me.name}"
package_managers = { package_managers = {
"npm" : "npm", "npm" : "npm",
"go" : "go", "go" : "go",

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.23" version = ">= 0.12.4"
} }
artifactory = { artifactory = {
source = "registry.terraform.io/jfrog/artifactory" source = "registry.terraform.io/jfrog/artifactory"
@@ -95,7 +95,7 @@ EOF
locals { locals {
# The username field to use for artifactory # The username field to use for artifactory
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
jfrog_host = replace(var.jfrog_url, "https://", "") jfrog_host = replace(var.jfrog_url, "https://", "")
} }
@@ -117,7 +117,6 @@ resource "artifactory_scoped_token" "me" {
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "jfrog" { resource "coder_script" "jfrog" {
agent_id = var.agent_id agent_id = var.agent_id
@@ -128,7 +127,7 @@ resource "coder_script" "jfrog" {
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id, JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email, ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token, ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server, CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""), REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),

1
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"bun-types": "^1.0.18", "bun-types": "^1.0.18",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^12.0.0", "marked": "^12.0.0",
"prettier": "^3.2.5",
"prettier-plugin-sh": "^0.13.1", "prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1" "prettier-plugin-terraform-formatter": "^1.2.1"
}, },

View File

@@ -11,7 +11,6 @@
"bun-types": "^1.0.18", "bun-types": "^1.0.18",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^12.0.0", "marked": "^12.0.0",
"prettier": "^3.2.5",
"prettier-plugin-sh": "^0.13.1", "prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1" "prettier-plugin-terraform-formatter": "^1.2.1"
}, },
@@ -24,4 +23,4 @@
"prettier-plugin-terraform-formatter" "prettier-plugin-terraform-formatter"
] ]
} }
} }

View File

@@ -8,7 +8,6 @@ import {
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
writeCoder,
} from "../test"; } from "../test";
describe("slackme", async () => { describe("slackme", async () => {
@@ -120,6 +119,15 @@ const setupContainer = async (
return { id, instance }; return { id, instance };
}; };
const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
};
const assertSlackMessage = async (opts: { const assertSlackMessage = async (opts: {
command: string; command: string;
format?: string; format?: string;

32
test.ts
View File

@@ -78,14 +78,6 @@ export const execContainer = async (
}; };
}; };
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
type TerraformStateResource = { type TerraformStateResource = {
type: string; type: string;
name: string; name: string;
@@ -178,19 +170,14 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
* random state file. * random state file.
*/ */
export const runTerraformApply = async < export const runTerraformApply = async <
TVars extends Readonly<Record<string, string | boolean>>, TVars extends Readonly<Record<string, string>>,
>( >(
dir: string, dir: string,
vars: TVars, vars: TVars,
env?: Record<string, string>,
): Promise<TerraformState> => { ): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
const combinedEnv = env === undefined ? {} : { ...env }; Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));
for (const [key, value] of Object.entries(vars)) {
combinedEnv[`TF_VAR_${key}`] = String(value);
}
const proc = spawn( const proc = spawn(
[ [
"terraform", "terraform",
@@ -204,18 +191,16 @@ export const runTerraformApply = async <
], ],
{ {
cwd: dir, cwd: dir,
env: combinedEnv, env,
stderr: "pipe", stderr: "pipe",
stdout: "pipe", stdout: "pipe",
}, },
); );
const text = await readableStreamToText(proc.stderr); const text = await readableStreamToText(proc.stderr);
const exitCode = await proc.exited; const exitCode = await proc.exited;
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error(text); throw new Error(text);
} }
const content = await readFile(stateFile, "utf8"); const content = await readFile(stateFile, "utf8");
await unlink(stateFile); await unlink(stateFile);
return JSON.parse(content); return JSON.parse(content);
@@ -243,12 +228,3 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
status: statusCode, status: statusCode,
}); });
}; };
export const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
};

View File

@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
```tf ```tf
module "vscode" { module "vscode" {
source = "registry.coder.com/modules/vscode-desktop/coder" source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.15" version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -28,7 +28,7 @@ module "vscode" {
```tf ```tf
module "vscode" { module "vscode" {
source = "registry.coder.com/modules/vscode-desktop/coder" source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.15" version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }

View File

@@ -18,57 +18,11 @@ describe("vscode-desktop", async () => {
agent_id: "foo", agent_id: "foo",
}); });
expect(state.outputs.vscode_url.value).toBe( expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", "vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
); );
const coder_app = state.resources.find( const resources = state.resources;
(res) => res.type == "coder_app" && res.name == "vscode", expect(resources[1].instances[0].attributes.order).toBeNull();
);
expect(coder_app).not.toBeNull();
expect(coder_app.instances.length).toBe(1);
expect(coder_app.instances[0].attributes.order).toBeNull();
});
it("adds folder", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder and open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
open_recent: "true",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds folder but not open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
openRecent: "false",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});
it("adds open_recent", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
open_recent: "true",
});
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
}); });
it("expect order to be set", async () => { it("expect order to be set", async () => {
@@ -77,11 +31,7 @@ describe("vscode-desktop", async () => {
order: "22", order: "22",
}); });
const coder_app = state.resources.find( const resources = state.resources;
(res) => res.type == "coder_app" && res.name == "vscode", expect(resources[1].instances[0].attributes.order).toBe(22);
);
expect(coder_app).not.toBeNull();
expect(coder_app.instances.length).toBe(1);
expect(coder_app.instances[0].attributes.order).toBe(22);
}); });
}); });

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.23" version = ">= 0.17"
} }
} }
} }
@@ -20,12 +20,6 @@ variable "folder" {
default = "" default = ""
} }
variable "open_recent" {
type = bool
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
default = false
}
variable "order" { variable "order" {
type = number type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
@@ -33,7 +27,6 @@ variable "order" {
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "vscode" { resource "coder_app" "vscode" {
agent_id = var.agent_id agent_id = var.agent_id
@@ -42,17 +35,22 @@ resource "coder_app" "vscode" {
slug = "vscode" slug = "vscode"
display_name = "VS Code Desktop" display_name = "VS Code Desktop"
order = var.order order = var.order
url = join("", [ url = var.folder != "" ? join("", [
"vscode://coder.coder-remote/open", "vscode://coder.coder-remote/open?owner=",
"?owner=", data.coder_workspace.me.owner,
data.coder_workspace_owner.me.name,
"&workspace=", "&workspace=",
data.coder_workspace.me.name, data.coder_workspace.me.name,
var.folder != "" ? join("", ["&folder=", var.folder]) : "", "&folder=",
var.open_recent ? "&openRecent" : "", var.folder,
"&url=", "&url=",
data.coder_workspace.me.access_url, data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN", "&token=$SESSION_TOKEN",
]) : join("", [
"vscode://coder.coder-remote/open?owner=",
data.coder_workspace.me.owner,
"&workspace=",
data.coder_workspace.me.name,
"&token=$SESSION_TOKEN",
]) ])
} }

View File

@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
```tf ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.14" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
accept_license = true accept_license = true
} }
@@ -29,7 +29,7 @@ module "vscode-web" {
```tf ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.14" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web" install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder" folder = "/home/coder"
@@ -42,7 +42,7 @@ module "vscode-web" {
```tf ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.14" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true accept_license = true
@@ -56,7 +56,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.14" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"] extensions = ["dracula-theme.theme-dracula"]
settings = { settings = {

View File

@@ -1,42 +0,0 @@
import { describe, expect, it } from "bun:test";
import { runTerraformApply, runTerraformInit } from "../test";
describe("vscode-web", async () => {
await runTerraformInit(import.meta.dir);
it("accept_license should be set to true", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "false",
});
};
expect(t).toThrow("Invalid value for variable");
});
it("use_cached and offline can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
use_cached: "true",
offline: "true",
});
};
expect(t).toThrow("Offline and Use Cached can not be used together");
});
it("offline and extensions can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
offline: "true",
extensions: '["1", "2"]',
});
};
expect(t).toThrow("Offline mode does not allow extensions to be installed");
});
// More tests depend on shebang refactors
});

View File

@@ -97,30 +97,6 @@ variable "settings" {
default = {} default = {}
} }
variable "offline" {
type = bool
description = "Just run VS Code Web in the background, don't fetch it from the internet."
default = false
}
variable "use_cached" {
type = bool
description = "Uses cached copy of VS Code Web in the background, otherwise fetches it from internet."
default = false
}
variable "extensions_dir" {
type = string
description = "Override the directory to store extensions in."
default = ""
}
variable "auto_install_extensions" {
type = bool
description = "Automatically install recommended extensions when VS Code Web starts."
default = false
}
resource "coder_script" "vscode-web" { resource "coder_script" "vscode-web" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "VS Code Web" display_name = "VS Code Web"
@@ -133,25 +109,8 @@ resource "coder_script" "vscode-web" {
TELEMETRY_LEVEL : var.telemetry_level, TELEMETRY_LEVEL : var.telemetry_level,
// This is necessary otherwise the quotes are stripped! // This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""), SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
}) })
run_on_start = true run_on_start = true
lifecycle {
precondition {
condition = !var.offline || length(var.extensions) == 0
error_message = "Offline mode does not allow extensions to be installed"
}
precondition {
condition = !var.offline || !var.use_cached
error_message = "Offline and Use Cached can not be used together"
}
}
} }
resource "coder_app" "vscode-web" { resource "coder_app" "vscode-web" {

View File

@@ -2,40 +2,6 @@
BOLD='\033[0;1m' BOLD='\033[0;1m'
EXTENSIONS=("${EXTENSIONS}") EXTENSIONS=("${EXTENSIONS}")
VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server"
# Set extension directory
EXTENSION_ARG=""
if [ -n "${EXTENSIONS_DIR}" ]; then
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
fi
run_vscode_web() {
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
}
# Check if the settings file exists...
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
echo "⚙️ Creating settings file..."
mkdir -p ~/.vscode-server/data/Machine
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
fi
# Check if vscode-server is already installed for offline or cached mode
if [ -f "$VSCODE_WEB" ]; then
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
echo "🥳 Found a copy of VS Code Web"
run_vscode_web
exit 0
fi
fi
# Offline mode always expects a copy of vscode-server to be present
if [ "${OFFLINE}" = true ]; then
echo "Failed to find a copy of VS Code Web"
exit 1
fi
# Create install prefix # Create install prefix
mkdir -p ${INSTALL_PREFIX} mkdir -p ${INSTALL_PREFIX}
@@ -60,7 +26,9 @@ if [ $? -ne 0 ]; then
echo "Failed to install Microsoft Visual Studio Code Server: $output" echo "Failed to install Microsoft Visual Studio Code Server: $output"
exit 1 exit 1
fi fi
printf "$${BOLD}VS Code Web has been installed.\n" printf "$${BOLD}Microsoft Visual Studio Code Server has been installed.\n"
VSCODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Install each extension... # Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
@@ -69,31 +37,20 @@ for extension in "$${EXTENSIONLIST[@]}"; do
continue continue
fi fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force) output=$($VSCODE_SERVER --install-extension "$extension" --force)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output" echo "Failed to install extension: $extension: $output"
exit 1 exit 1
fi fi
done done
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then # Check if the settings file exists...
if ! command -v jq > /dev/null; then if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
echo "jq is required to install extensions from a workspace file." echo "⚙️ Creating settings file..."
exit 0 mkdir -p ~/.vscode-server/data/Machine
fi echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
WORKSPACE_DIR="$HOME"
if [ -n "${FOLDER}" ]; then
WORKSPACE_DIR="${FOLDER}"
fi
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
for extension in $extensions; do
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
done
fi
fi fi
run_vscode_web echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
"${INSTALL_PREFIX}/bin/code-server" serve-local --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &

View File

@@ -1,57 +1,35 @@
--- ---
display_name: Windows RDP display_name: Windows RDP
description: RDP Server and Web Client, powered by Devolutions Gateway description: RDP Server and Web Client powered by Devolutions
icon: ../.icons/desktop.svg icon: ../.icons/desktop.svg
maintainer_github: coder maintainer_github: coder
verified: true verified: false
tags: [windows, rdp, web, desktop] tags: [windows, rdp, web, desktop]
--- ---
# Windows RDP # Windows RDP
Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway). Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway)
```tf [![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538)
# AWS example. See below for examples of using this module with other providers
module "windows_rdp" {
source = "registry.coder.com/coder/module/windows-rdp"
version = "1.0.16"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
```
## Video ## Usage
[![Video](./video-thumbnails/video-thumbnail.png)](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02)
## Examples
### With AWS
```tf ```tf
module "windows_rdp" { module "windows_rdp" {
source = "registry.coder.com/modules/windows-rdp/coder" count = data.coder_workspace.me.start_count
version = "1.0.16" source = "github.com/coder/modules//windows-rdp?ref=web-rdp"
count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
```
### With Google Cloud
```tf
module "windows_rdp" {
source = "registry.coder.com/modules/windows-rdp/coder"
version = "1.0.16"
count = data.coder_workspace.me.start_count
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id resource_id = resource.google_compute_instance.dev[0].id
} }
``` ```
## Tested on
- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143)
## Roadmap ## Roadmap
- [ ] Test on Microsoft Azure. - [ ] Test on additional cloud providers
- [ ] Automatically establish web RDP session when users click "web RDP"
> This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality.

View File

@@ -12,10 +12,11 @@
* - A lot of the HTML selectors in this file will look nonstandard. This is * - A lot of the HTML selectors in this file will look nonstandard. This is
* because they are actually custom Angular components. * because they are actually custom Angular components.
* - It is strongly advised that you avoid template literals that use the * - It is strongly advised that you avoid template literals that use the
* placeholder syntax via the dollar sign. The Terraform file is treating this * placeholder syntax via the dollar sign. The Terraform script looks for
* as a template file, and because it also uses a similar syntax, there's a * these characters so that it can inject Coder-specific values, so any
* risk that some values will trigger false positives. If a template literal * template literal that uses the character actually needs to double up each
* must be used, be sure to use a double dollar sign to escape things. * of them. There are already a few places in this file where it couldn't be
* avoided, but avoiding this as much as possible will save you some headache.
* - All the CSS should be written via custom style tags and the !important * - All the CSS should be written via custom style tags and the !important
* directive (as much as that is a bad idea most of the time). We do not * directive (as much as that is a bad idea most of the time). We do not
* control the Angular app, so we have to modify things from afar to ensure * control the Angular app, so we have to modify things from afar to ensure

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it, test } from "bun:test";
import { import {
TerraformState, executeScriptInContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@@ -13,30 +13,6 @@ type TestVariables = Readonly<{
admin_password?: string; admin_password?: string;
}>; }>;
function findWindowsRdpScript(state: TerraformState): string | null {
for (const resource of state.resources) {
const isRdpScriptResource =
resource.type === "coder_script" && resource.name === "windows-rdp";
if (!isRdpScriptResource) {
continue;
}
for (const instance of resource.instances) {
if (instance.attributes.display_name === "windows-rdp") {
return instance.attributes.script;
}
}
}
return null;
}
/**
* @todo It would be nice if we had a way to verify that the Devolutions root
* HTML file is modified to include the import for the patched Coder script,
* but the current test setup doesn't really make that viable
*/
describe("Web RDP", async () => { describe("Web RDP", async () => {
await runTerraformInit(import.meta.dir); await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(import.meta.dir, { testRequiredVariables<TestVariables>(import.meta.dir, {
@@ -44,48 +20,30 @@ describe("Web RDP", async () => {
resource_id: "bar", resource_id: "bar",
}); });
it("Has the PowerShell script install Devolutions Gateway", async () => { it("Installs the Devolutions Gateway Angular app locally on the machine", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, { const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
resource_id: "bar", resource_id: "bar",
}); });
const lines = findWindowsRdpScript(state) throw new Error("Not implemented yet");
?.split("\n") });
.filter(Boolean)
.map((line) => line.trim());
expect(lines).toEqual( /**
expect.arrayContaining<string>([ * @todo Verify that the HTML file has been modified, and that the JS file is
'$moduleName = "DevolutionsGateway"', * also part of the file system
// Devolutions does versioning in the format year.minor.patch */
expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => {
"Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", const state = await runTerraformApply<TestVariables>(import.meta.dir, {
]), agent_id: "foo",
); resource_id: "bar",
});
throw new Error("Not implemented yet");
}); });
it("Injects Terraform's username and password into the JS patch file", async () => { it("Injects Terraform's username and password into the JS patch file", async () => {
/** throw new Error("Not implemented yet");
* Using a regex as a quick-and-dirty way to get at the username and
* password values.
*
* Tried going through the trouble of extracting out the form entries
* variable from the main output, converting it from Prettier/JS-based JSON
* text to universal JSON text, and exposing it as a parsed JSON value. That
* got to be a bit too much, though.
*
* Regex is a little bit more verbose and pedantic than normal. Want to
* have some basic safety nets for validating the structure of the form
* entries variable after the JS file has had values injected. Even with all
* the wildcard classes set to lazy mode, we want to make sure that they
* don't overshoot and grab too much content.
*
* Written and tested via Regex101
* @see {@link https://regex101.com/r/UMgQpv/2}
*/
const formEntryValuesRe =
/^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?<username>.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?<password>.+?)",$.*?^};$/ms;
// Test that things work with the default username/password // Test that things work with the default username/password
const defaultState = await runTerraformApply<TestVariables>( const defaultState = await runTerraformApply<TestVariables>(
@@ -96,35 +54,19 @@ describe("Web RDP", async () => {
}, },
); );
const defaultRdpScript = findWindowsRdpScript(defaultState); const output = await executeScriptInContainer(defaultState, "alpine");
expect(defaultRdpScript).toBeString();
const { username: defaultUsername, password: defaultPassword } =
formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {};
expect(defaultUsername).toBe("Administrator");
expect(defaultPassword).toBe("coderRDP!");
// Test that custom usernames/passwords are also forwarded correctly // Test that custom usernames/passwords are also forwarded correctly
const customAdminUsername = "crouton"; const customUsername = "crouton";
const customAdminPassword = "VeryVeryVeryVeryVerySecurePassword97!"; const customPassword = "VeryVeryVeryVeryVerySecurePassword97!";
const customizedState = await runTerraformApply<TestVariables>( const customizedState = await runTerraformApply<TestVariables>(
import.meta.dir, import.meta.dir,
{ {
agent_id: "foo", agent_id: "foo",
resource_id: "bar", resource_id: "bar",
admin_username: customAdminUsername, admin_username: customUsername,
admin_password: customAdminPassword, admin_password: customPassword,
}, },
); );
const customRdpScript = findWindowsRdpScript(customizedState);
expect(customRdpScript).toBeString();
const { username: customUsername, password: customPassword } =
formEntryValuesRe.exec(customRdpScript)?.groups ?? {};
expect(customUsername).toBe(customAdminUsername);
expect(customPassword).toBe(customAdminPassword);
}); });
}); });

View File

@@ -33,20 +33,94 @@ variable "admin_password" {
resource "coder_script" "windows-rdp" { resource "coder_script" "windows-rdp" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "windows-rdp" display_name = "windows-rdp"
icon = "/icon/desktop.svg" icon = "https://svgur.com/i/158F.svg" # TODO: add to Coder icons
script = <<EOF
function Set-AdminPassword {
param (
[string]$adminPassword
)
# Set admin password
Get-LocalUser -Name "${var.admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
# Enable admin user
Get-LocalUser -Name "${var.admin_username}" | Enable-LocalUser
}
script = templatefile("${path.module}/powershell-installation-script.tftpl", { function Configure-RDP {
admin_username = var.admin_username # Enable RDP
admin_password = var.admin_password New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -PropertyType DWORD -Force
# Disable NLA
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -PropertyType DWORD -Force
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
# Enable RDP through Windows Firewall
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
}
# Wanted to have this be in the powershell template file, but Terraform function Install-DevolutionsGateway {
# doesn't allow recursive calls to the templatefile function. Have to feed # Define the module name and version
# results of the JS template replace into the powershell template $moduleName = "DevolutionsGateway"
patch_file_contents = templatefile("${path.module}/devolutions-patch.js", { $moduleVersion = "2024.1.5"
CODER_USERNAME = var.admin_username
CODER_PASSWORD = var.admin_password Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force -Confirm:$false -SkipPublisherCheck
})
}) try {
# Try to import the module directly
Import-Module $moduleName -ErrorAction Stop
} catch {
# If it fails, install and then import the module
# Construct the module path for system-wide installation
$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion"
$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1"
# Import the module using the full path
Import-Module $modulePath
}
Install-DGatewayPackage
# Configure Devolutions Gateway
$Hostname = "localhost"
$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171'
$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None
$ConfigParams = @{
Hostname = $Hostname
Listeners = @($HttpListener)
WebApp = $WebApp
}
Set-DGatewayConfig @ConfigParams
New-DGatewayProvisionerKeyPair -Force
# Configure and start the Windows service
Set-Service 'DevolutionsGateway' -StartupType 'Automatic'
Start-Service 'DevolutionsGateway'
}
function Patch-Devolutions-HTML {
$root = "C:\Program Files\Devolutions\Gateway\webapp\client"
$devolutionsHtml = "$root\index.html"
$patch = '<script defer id="coder-patch" src="coder.js"></script>'
# Always copy the file in case we change it.
@'
${templatefile("${path.module}/devolutions-patch.js", {
CODER_USERNAME : var.admin_username,
CODER_PASSWORD : var.admin_password,
})}
'@ | Set-Content "$root\coder.js"
# Only inject the src if we have not before.
$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch
if ($isPatched -eq $null) {
(Get-Content $devolutionsHtml).Replace('</app-root>', "</app-root>$patch") | Set-Content $devolutionsHtml
}
}
Set-AdminPassword -adminPassword "${var.admin_password}"
Configure-RDP
Install-DevolutionsGateway
Patch-Devolutions-HTML
EOF
run_on_start = true run_on_start = true
} }
@@ -56,7 +130,7 @@ resource "coder_app" "windows-rdp" {
slug = "web-rdp" slug = "web-rdp"
display_name = "Web RDP" display_name = "Web RDP"
url = "http://localhost:7171" url = "http://localhost:7171"
icon = "/icon/desktop.svg" icon = "https://svgur.com/i/158F.svg"
subdomain = true subdomain = true
healthcheck { healthcheck {
@@ -71,6 +145,29 @@ resource "coder_app" "rdp-docs" {
display_name = "Local RDP" display_name = "Local RDP"
slug = "rdp-docs" slug = "rdp-docs"
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop" url = "https://coder.com/docs/v2/latest/ides/remote-desktops#rdp-desktop"
external = true external = true
} }
# For some reason this is not rendering, commented out for now
# resource "coder_metadata" "rdp_details" {
# resource_id = var.resource_id
# daily_cost = 0
# item {
# key = "Host"
# value = "localhost"
# }
# item {
# key = "Port"
# value = "3389"
# }
# item {
# key = "Username"
# value = "Administrator"
# }
# item {
# key = "Password"
# value = var.admin_password
# sensitive = true
# }
# }

View File

@@ -1,85 +0,0 @@
function Set-AdminPassword {
param (
[string]$adminPassword
)
# Set admin password
Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force)
# Enable admin user
Get-LocalUser -Name "${admin_username}" | Enable-LocalUser
}
function Configure-RDP {
# Enable RDP
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -PropertyType DWORD -Force
# Disable NLA
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -PropertyType DWORD -Force
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
# Enable RDP through Windows Firewall
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
}
function Install-DevolutionsGateway {
# Define the module name and version
$moduleName = "DevolutionsGateway"
$moduleVersion = "2024.1.5"
# Install the module with the specified version for all users
# This requires administrator privileges
try {
# Install-PackageProvider is required for AWS. Need to set command to
# terminate on failure so that try/catch actually triggers
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop
Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force
}
catch {
# If the first command failed, assume that we're on GCP and run
# Install-Module only
Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force
}
# Construct the module path for system-wide installation
$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion"
$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1"
# Import the module using the full path
Import-Module $modulePath
Install-DGatewayPackage
# Configure Devolutions Gateway
$Hostname = "localhost"
$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171'
$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None
$ConfigParams = @{
Hostname = $Hostname
Listeners = @($HttpListener)
WebApp = $WebApp
}
Set-DGatewayConfig @ConfigParams
New-DGatewayProvisionerKeyPair -Force
# Configure and start the Windows service
Set-Service 'DevolutionsGateway' -StartupType 'Automatic'
Start-Service 'DevolutionsGateway'
}
function Patch-Devolutions-HTML {
$root = "C:\Program Files\Devolutions\Gateway\webapp\client"
$devolutionsHtml = "$root\index.html"
$patch = '<script defer id="coder-patch" src="coder.js"></script>'
# Always copy the file in case we change it.
@'
${patch_file_contents}
'@ | Set-Content "$root\coder.js"
# Only inject the src if we have not before.
$isPatched = Select-String -Path "$devolutionsHtml" -Pattern "$patch" -SimpleMatch
if ($isPatched -eq $null) {
(Get-Content $devolutionsHtml).Replace('</app-root>', "</app-root>$patch") | Set-Content $devolutionsHtml
}
}
Set-AdminPassword -adminPassword "${admin_password}"
Configure-RDP
Install-DevolutionsGateway
Patch-Devolutions-HTML

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB