Compare commits

..

2 Commits

Author SHA1 Message Date
Kyle Carberry
cd3d42d54e Fmt 2023-10-11 12:46:03 -05:00
Kyle Carberry
66f1c3b97c slackme: exit with command code and quote command 2023-10-11 12:38:29 -05:00
101 changed files with 759 additions and 3354 deletions

View File

@@ -20,8 +20,6 @@ jobs:
- uses: oven-sh/setup-bun@v1 - uses: oven-sh/setup-bun@v1
with: with:
bun-version: latest bun-version: latest
- name: Setup
run: bun install
- run: bun test - run: bun test
pretty: pretty:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -30,11 +28,7 @@ jobs:
- uses: oven-sh/setup-bun@v1 - uses: oven-sh/setup-bun@v1
with: with:
bun-version: latest bun-version: latest
- name: Setup
run: bun install
- name: Format - name: Format
run: bun fmt:ci run: bun fmt:ci
- name: typos-action
uses: crate-ci/typos@v1.17.2
- name: Lint - name: Lint
run: bun lint run: bun install && bun lint

View File

@@ -1,42 +0,0 @@
name: Update README on Tag
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
update-readme:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get the latest tag
id: get-latest-tag
run: echo "TAG=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_OUTPUT
- name: Run update script
run: ./update-version.sh
- name: Create Pull Request
id: create-pr
uses: peter-evans/create-pull-request@v5
with:
commit-message: 'chore: bump version to ${{ env.TAG }} in README.md files'
title: 'chore: bump version to ${{ env.TAG }} in README.md files'
body: 'This is an auto-generated PR to update README.md files of all modules with the new tag ${{ env.TAG }}'
branch: 'update-readme-branch'
base: 'main'
env:
TAG: ${{ steps.get-latest-tag.outputs.TAG }}
- name: Auto-approve
uses: hmarr/auto-approve-action@v4
if: github.ref == 'refs/heads/update-readme-branch'

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68.03 68.03"><defs><style>.cls-1{fill:#da291c;}</style></defs><title>Artboard 1</title><polygon class="cls-1" points="34.02 13.31 11.27 52.72 14.52 52.72 34.02 18.94 34.02 24.57 17.77 52.72 21.02 52.72 34.02 30.2 34.02 35.83 24.27 52.72 27.52 52.72 34.02 41.46 34.02 47.09 30.77 52.72 34.02 52.72 34.02 52.72 56.77 52.72 34.02 13.31"/></svg>

Before

Width:  |  Height:  |  Size: 427 B

View File

@@ -1 +0,0 @@
<svg width="2270" height="2500" viewBox="0 0 256 282" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><g fill="#8CC84B"><path d="M116.504 3.58c6.962-3.985 16.03-4.003 22.986 0 34.995 19.774 70.001 39.517 104.99 59.303 6.581 3.707 10.983 11.031 10.916 18.614v118.968c.049 7.897-4.788 15.396-11.731 19.019-34.88 19.665-69.742 39.354-104.616 59.019-7.106 4.063-16.356 3.75-23.24-.646-10.457-6.062-20.932-12.094-31.39-18.15-2.137-1.274-4.546-2.288-6.055-4.36 1.334-1.798 3.719-2.022 5.657-2.807 4.365-1.388 8.374-3.616 12.384-5.778 1.014-.694 2.252-.428 3.224.193 8.942 5.127 17.805 10.403 26.777 15.481 1.914 1.105 3.852-.362 5.488-1.274 34.228-19.345 68.498-38.617 102.72-57.968 1.268-.61 1.969-1.956 1.866-3.345.024-39.245.006-78.497.012-117.742.145-1.576-.767-3.025-2.192-3.67-34.759-19.575-69.5-39.18-104.253-58.76a3.621 3.621 0 0 0-4.094-.006C91.2 39.257 56.465 58.88 21.712 78.454c-1.42.646-2.373 2.071-2.204 3.653.006 39.245 0 78.497 0 117.748a3.329 3.329 0 0 0 1.89 3.303c9.274 5.259 18.56 10.481 27.84 15.722 5.228 2.814 11.647 4.486 17.407 2.33 5.083-1.823 8.646-7.01 8.549-12.407.048-39.016-.024-78.038.036-117.048-.127-1.732 1.516-3.163 3.2-3 4.456-.03 8.918-.06 13.374.012 1.86-.042 3.14 1.823 2.91 3.568-.018 39.263.048 78.527-.03 117.79.012 10.464-4.287 21.85-13.966 26.97-11.924 6.177-26.662 4.867-38.442-1.056-10.198-5.09-19.93-11.097-29.947-16.55C5.368 215.886.555 208.357.604 200.466V81.497c-.073-7.74 4.504-15.197 11.29-18.85C46.768 42.966 81.636 23.27 116.504 3.58z"/><path d="M146.928 85.99c15.21-.979 31.493-.58 45.18 6.913 10.597 5.742 16.472 17.793 16.659 29.566-.296 1.588-1.956 2.464-3.472 2.355-4.413-.006-8.827.06-13.24-.03-1.872.072-2.96-1.654-3.195-3.309-1.268-5.633-4.34-11.212-9.642-13.929-8.139-4.075-17.576-3.87-26.451-3.785-6.479.344-13.446.905-18.935 4.715-4.214 2.886-5.494 8.712-3.99 13.404 1.418 3.369 5.307 4.456 8.489 5.458 18.33 4.794 37.754 4.317 55.734 10.626 7.444 2.572 14.726 7.572 17.274 15.366 3.333 10.446 1.872 22.932-5.56 31.318-6.027 6.901-14.805 10.657-23.56 12.697-11.647 2.597-23.734 2.663-35.562 1.51-11.122-1.268-22.696-4.19-31.282-11.768-7.342-6.375-10.928-16.308-10.572-25.895.085-1.619 1.697-2.748 3.248-2.615 4.444-.036 8.888-.048 13.332.006 1.775-.127 3.091 1.407 3.182 3.08.82 5.367 2.837 11 7.517 14.182 9.032 5.827 20.365 5.428 30.707 5.591 8.568-.38 18.186-.495 25.178-6.158 3.689-3.23 4.782-8.634 3.785-13.283-1.08-3.925-5.186-5.754-8.712-6.95-18.095-5.724-37.736-3.647-55.656-10.12-7.275-2.571-14.31-7.432-17.105-14.906-3.9-10.578-2.113-23.662 6.098-31.765 8.006-8.06 19.563-11.164 30.551-12.275z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#FFD814" d="M0 0l7.971 15.516L16 0H0zm6.732 6.16h-1.27V4.89h1.27v1.27zm0-1.906h-1.27V2.985h1.27v1.269zm1.904 3.81h-1.27v-1.27h1.27v1.27zm0-1.905h-1.27V4.89h1.27v1.27zm0-1.905h-1.27V2.985h1.27v1.269zm1.894 1.905H9.26V4.89h1.27v1.27zM9.26 4.254V2.985h1.27v1.269H9.26z"/></svg>

Before

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -11,10 +11,9 @@ tags: [helper]
<!-- Describes what this module does --> <!-- Describes what this module does -->
```tf ```hcl
module "MODULE_NAME" { module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder" source = "https://registry.coder.com/modules/MODULE_NAME"
version = "1.0.2"
} }
``` ```
@@ -26,10 +25,9 @@ module "MODULE_NAME" {
Install the Dracula theme from [OpenVSX](https://open-vsx.org/): Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf ```hcl
module "MODULE_NAME" { module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder" source = "https://registry.coder.com/modules/MODULE_NAME"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ extensions = [
"dracula-theme.theme-dracula" "dracula-theme.theme-dracula"
@@ -43,10 +41,9 @@ Enter the `<author>.<name>` into the extensions array and code-server will autom
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file: Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
```tf ```hcl
module "MODULE_NAME" { module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder" source = "https://registry.coder.com/modules/MODULE_NAME"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ "dracula-theme.theme-dracula" ] extensions = [ "dracula-theme.theme-dracula" ]
settings = { settings = {
@@ -59,10 +56,9 @@ module "MODULE_NAME" {
Run code-server in the background, don't fetch it from GitHub: Run code-server in the background, don't fetch it from GitHub:
```tf ```hcl
module "MODULE_NAME" { module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder" source = "https://registry.coder.com/modules/MODULE_NAME"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
offline = true offline = true
} }

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.12"
} }
} }
} }
@@ -50,12 +50,6 @@ variable "mutable" {
description = "Whether the parameter is mutable." description = "Whether the parameter is mutable."
default = true default = true
} }
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
# Add other variables here # Add other variables here
@@ -75,10 +69,9 @@ resource "coder_app" "MODULE_NAME" {
slug = "MODULE_NAME" slug = "MODULE_NAME"
display_name = "MODULE_NAME" display_name = "MODULE_NAME"
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = local.icon_url icon = loocal.icon_url
subdomain = false subdomain = false
share = "owner" share = "owner"
order = var.order
# Remove if the app does not have a healthcheck endpoint # Remove if the app does not have a healthcheck endpoint
healthcheck { healthcheck {

View File

@@ -1,15 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env bash
# Convert templated variables to shell variables
# shellcheck disable=SC2269
LOG_PATH=${LOG_PATH}
# shellcheck disable=SC2034
BOLD='\033[0;1m' BOLD='\033[0;1m'
# shellcheck disable=SC2059
printf "$${BOLD}Installing MODULE_NAME ...\n\n" printf "$${BOLD}Installing MODULE_NAME ...\n\n"
# Add code here # Add code here
# Use varibles from the templatefile function in main.tf # Use varibles from the templatefile function in main.tf
# e.g. LOG_PATH, PORT, etc. # e.g. LOG_PATH, PORT, etc.
@@ -21,6 +13,6 @@ printf "👷 Starting MODULE_NAME in background...\n\n"
# 1. Use & to run it in background # 1. Use & to run it in background
# 2. redirct stdout and stderr to log files # 2. redirct stdout and stderr to log files
./app > "$${LOG_PATH}" 2>&1 & ./app >${LOG_PATH} 2>&1 &
printf "check logs at %s\n\n" "$${LOG_PATH}" printf "check logs at ${LOG_PATH} \n\n"

View File

@@ -3,7 +3,7 @@
To create a new module, clone this repository and run: To create a new module, clone this repository and run:
```shell ```shell
./new.sh MODULE_NAME ./new.sh MOUDLE_NAME
``` ```
## Testing a Module ## Testing a Module
@@ -19,10 +19,8 @@ $ bun test -t '<module>'
You can test a module locally by updating the source as follows You can test a module locally by updating the source as follows
```tf ```hcl
module "example" { source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
}
``` ```
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR. > **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.

View File

@@ -14,10 +14,9 @@ Modules extend Templates to create reusable components for your development envi
e.g. e.g.
```tf ```hcl
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.2"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```

View File

@@ -14,10 +14,9 @@ the region closest to them.
Customize the preselected parameter value: Customize the preselected parameter value:
```tf ```hcl
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "https://registry.coder.com/modules/aws-region"
version = "1.0.2"
default = "us-east-1" default = "us-east-1"
} }
@@ -34,18 +33,15 @@ provider "aws" {
Change the display name and icon for a region using the corresponding maps: Change the display name and icon for a region using the corresponding maps:
```tf ```hcl
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "https://registry.coder.com/modules/aws-region"
version = "1.0.2"
default = "ap-south-1" default = "ap-south-1"
custom_names = { custom_names = {
"ap-south-1" : "Awesome Mumbai!" "ap-south-1": "Awesome Mumbai!"
} }
custom_icons = { custom_icons = {
"ap-south-1" : "/emojis/1f33a.png" "ap-south-1": "/emojis/1f33a.png"
} }
} }
@@ -60,11 +56,10 @@ provider "aws" {
Hide the Asia Pacific regions Seoul and Osaka: Hide the Asia Pacific regions Seoul and Osaka:
```tf ```hcl
module "aws-region" { module "aws-region" {
source = "registry.coder.com/modules/aws-region/coder" source = "https://registry.coder.com/modules/aws-region"
version = "1.0.2" exclude = [ "ap-northeast-2", "ap-northeast-3" ]
exclude = ["ap-northeast-2", "ap-northeast-3"]
} }
provider "aws" { provider "aws" {

View File

@@ -11,10 +11,9 @@ tags: [helper, parameter, azure, regions]
This module adds a parameter with all Azure regions, allowing developers to select the region closest to them. This module adds a parameter with all Azure regions, allowing developers to select the region closest to them.
```tf ```hcl
module "azure_region" { module "azure_region" {
source = "registry.coder.com/modules/azure-region/coder" source = "https://registry.coder.com/modules/azure-region"
version = "1.0.2"
default = "eastus" default = "eastus"
} }
@@ -31,15 +30,14 @@ resource "azurem_resource_group" "example" {
Change the display name and icon for a region using the corresponding maps: Change the display name and icon for a region using the corresponding maps:
```tf ```hcl
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "https://registry.coder.com/modules/azure-region"
version = "1.0.2"
custom_names = { custom_names = {
"australia" : "Go Australia!" "australia": "Go Australia!"
} }
custom_icons = { custom_icons = {
"australia" : "/icons/smiley.svg" "australia": "/icons/smiley.svg"
} }
} }
@@ -54,10 +52,9 @@ resource "azurerm_resource_group" "example" {
Hide all regions in Australia except australiacentral: Hide all regions in Australia except australiacentral:
```tf ```hcl
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "https://registry.coder.com/modules/azure-region"
version = "1.0.2"
exclude = [ exclude = [
"australia", "australia",
"australiacentral2", "australiacentral2",

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,10 +11,9 @@ tags: [helper, ide, web]
Automatically install [code-server](https://github.com/coder/code-server) in a workspace, create an app to access it via the dashboard, install extensions, and pre-configure editor settings. Automatically install [code-server](https://github.com/coder/code-server) in a workspace, create an app to access it via the dashboard, install extensions, and pre-configure editor settings.
```tf ```hcl
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -25,10 +24,9 @@ module "code-server" {
### Pin Versions ### Pin Versions
```tf ```hcl
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_version = "4.8.3" install_version = "4.8.3"
} }
@@ -38,10 +36,9 @@ module "code-server" {
Install the Dracula theme from [OpenVSX](https://open-vsx.org/): Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf ```hcl
module "code-server" { module "code-server" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = [ extensions = [
"dracula-theme.theme-dracula" "dracula-theme.theme-dracula"
@@ -55,27 +52,25 @@ Enter the `<author>.<name>` into the extensions array and code-server will autom
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file: Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
```tf ```hcl
module "code-server" { module "settings" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"] extensions = [ "dracula-theme.theme-dracula" ]
settings = { settings = {
"workbench.colorTheme" = "Dracula" "workbench.colorTheme" = "Dracula"
} }
} }
``` ```
### Install multiple extensions ### Offline Mode
Just run code-server in the background, don't fetch it from GitHub: Just run code-server in the background, don't fetch it from GitHub:
```tf ```hcl
module "code-server" { module "settings" {
source = "registry.coder.com/modules/code-server/coder" source = "https://registry.coder.com/modules/code-server"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] offline = true
} }
``` ```

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.12"
} }
} }
} }
@@ -32,12 +32,6 @@ variable "display_name" {
default = "code-server" default = "code-server"
} }
variable "slug" {
type = string
description = "The slug for the code-server application."
default = "code-server"
}
variable "settings" { variable "settings" {
type = map(string) type = map(string)
description = "A map of settings to apply to code-server." description = "A map of settings to apply to code-server."
@@ -68,21 +62,6 @@ variable "install_version" {
default = "" default = ""
} }
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" "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"
@@ -90,7 +69,6 @@ resource "coder_script" "code-server" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
VERSION : var.install_version, VERSION : var.install_version,
EXTENSIONS : join(",", var.extensions), EXTENSIONS : join(",", var.extensions),
APP_NAME : var.display_name,
PORT : var.port, PORT : var.port,
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
INSTALL_PREFIX : var.install_prefix, INSTALL_PREFIX : var.install_prefix,
@@ -102,13 +80,12 @@ resource "coder_script" "code-server" {
resource "coder_app" "code-server" { resource "coder_app" "code-server" {
agent_id = var.agent_id agent_id = var.agent_id
slug = var.slug slug = "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 = false subdomain = false
share = var.share share = "owner"
order = var.order
healthcheck { healthcheck {
url = "http://localhost:${var.port}/healthz" url = "http://localhost:${var.port}/healthz"

View File

@@ -25,8 +25,7 @@ printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server" CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Install each extension... # Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" for extension in "$${EXTENSIONS[@]}"; do
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then if [ -z "$extension" ]; then
continue continue
fi fi
@@ -39,12 +38,12 @@ for extension in "$${EXTENSIONLIST[@]}"; do
done done
# Check if the settings file exists... # Check if the settings file exists...
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then if [ ! -f ~/.local/share/code-server/Machine/settings.json ]; then
echo "⚙️ Creating settings file..." echo "⚙️ Creating settings file..."
mkdir -p ~/.local/share/code-server/User mkdir -p ~/.local/share/code-server/Machine
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json echo "${SETTINGS}" >~/.local/share/code-server/Machine/settings.json
fi fi
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 --auth none --port ${PORT} --app-name "${APP_NAME}" > ${LOG_PATH} 2>&1 & $CODE_SERVER --auth none --port ${PORT} >${LOG_PATH} 2>&1 &

View File

@@ -11,10 +11,9 @@ tags: [helper]
Automatically logs the user into Coder when creating their workspace. Automatically logs the user into Coder when creating their workspace.
```tf ```hcl
module "coder-login" { module "coder-login" {
source = "registry.coder.com/modules/coder-login/coder" source = "https://registry.coder.com/modules/coder-login"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -7,9 +7,8 @@ BOLD='\033[0;1m'
printf "$${BOLD}Logging into Coder...\n\n$${RESET}" printf "$${BOLD}Logging into Coder...\n\n$${RESET}"
if ! coder list > /dev/null 2>&1; then if ! coder list >/dev/null 2>&1; then
set +x set +x; coder login --token="${CODER_USER_TOKEN}" --url="${CODER_DEPLOYMENT_URL}"
coder login --token="${CODER_USER_TOKEN}" --url="${CODER_DEPLOYMENT_URL}"
else else
echo "You are already authenticated with coder." echo "You are already authenticated with coder."
fi fi

View File

@@ -11,10 +11,9 @@ tags: [helper]
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. 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.
```tf ```hcl
module "dotfiles" { module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder" source = "https://registry.coder.com/modules/dotfiles"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -1,114 +0,0 @@
---
display_name: exoscale-instance-type
description: A parameter with human readable exoscale instance names
icon: ../.icons/exoscale.svg
maintainer_github: WhizUs
verified: false
tags: [helper, parameter, instances, exoscale]
---
# exoscale-instance-type
A parameter with all Exoscale instance types. This allows developers to select
their desired virtual machine for the workspace.
Customize the preselected parameter value:
```tf
module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.2"
default = "standard.medium"
}
resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value
# ...
}
resource "coder_metadata" "workspace_info" {
item {
key = "instance type"
value = module.exoscale-instance-type.name
}
}
```
![Exoscale instance types](../.images/exoscale-instance-types.png)
## Examples
### Customize type
Change the display name a type using the corresponding maps:
```tf
module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.2"
default = "standard.medium"
custom_names = {
"standard.medium" : "Mittlere Instanz" # German translation
}
custom_descriptions = {
"standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation
}
}
resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value
# ...
}
resource "coder_metadata" "workspace_info" {
item {
key = "instance type"
value = module.exoscale-instance-type.name
}
}
```
![Exoscale instance types Custom](../.images/exoscale-instance-custom.png)
### Use category and exclude type
Show only gpu1 types
```tf
module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.2"
default = "gpu.large"
type_category = ["gpu"]
exclude = [
"gpu2.small",
"gpu2.medium",
"gpu2.large",
"gpu2.huge",
"gpu3.small",
"gpu3.medium",
"gpu3.large",
"gpu3.huge"
]
}
resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value
# ...
}
resource "coder_metadata" "workspace_info" {
item {
key = "instance type"
value = module.exoscale-instance-type.name
}
}
```
![Exoscale instance types category and exclude](../.images/exoscale-instance-exclude.png)
## Related templates
A related exoscale template will be provided soon.

View File

@@ -1,34 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("exoscale-instance-type", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
default: "gpu3.huge",
type_category: `["gpu", "cpu"]`,
});
expect(state.outputs.value.value).toBe("gpu3.huge");
});
it("fails because of wrong categroy definition", async () => {
expect(async () => {
await runTerraformApply(import.meta.dir, {
default: "gpu3.huge",
// type_category: ["standard"] is standard
});
}).toThrow('default value "gpu3.huge" must be defined as one of options');
});
});

View File

@@ -1,279 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "display_name" {
default = "Exoscale instance type"
description = "The display name of the parameter."
type = string
}
variable "description" {
default = "Select the exoscale instance type to use for the workspace. Check out the pricing page for more information: https://www.exoscale.com/pricing"
description = "The description of the parameter."
type = string
}
variable "default" {
default = ""
description = "The default instance type to use if no type is specified. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for instance type IDs."
type = map(string)
}
variable "custom_descriptions" {
default = {}
description = "A map of custom descriptions for instance type IDs."
type = map(string)
}
variable "type_category" {
default = ["standard"]
description = "A list of instance type categories the user is allowed to choose. One of [\"standard\", \"cpu\", \"memory\", \"storage\", \"gpu\"]"
type = list(string)
}
variable "exclude" {
default = []
description = "A list of instance type IDs to exclude. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
type = list(string)
}
locals {
# https://www.exoscale.com/pricing/
standard_instances = [
{
value = "standard.micro",
name = "Standard Micro",
description = "512 MB RAM, 1 Core, 10 - 200 GB Disk"
},
{
value = "standard.tiny",
name = "Standard Tiny",
description = "1 GB RAM, 1 Core, 10 - 400 GB Disk"
},
{
value = "standard.small",
name = "Standard Small",
description = "2 GB RAM, 2 Cores, 10 - 400 GB Disk"
},
{
value = "standard.medium",
name = "Standard Medium",
description = "4 GB RAM, 2 Cores, 10 - 400 GB Disk"
},
{
value = "standard.large",
name = "Standard Large",
description = "8 GB RAM, 4 Cores, 10 - 400 GB Disk"
},
{
value = "standard.extra",
name = "Standard Extra",
description = "rge",
description = "16 GB RAM, 4 Cores, 10 - 800 GB Disk"
},
{
value = "standard.huge",
name = "Standard Huge",
description = "32 GB RAM, 8 Cores, 10 - 800 GB Disk"
},
{
value = "standard.mega",
name = "Standard Mega",
description = "64 GB RAM, 12 Cores, 10 - 800 GB Disk"
},
{
value = "standard.titan",
name = "Standard Titan",
description = "128 GB RAM, 16 Cores, 10 - 1.6 TB Disk"
},
{
value = "standard.jumbo",
name = "Standard Jumbo",
description = "256 GB RAM, 24 Cores, 10 - 1.6 TB Disk"
},
{
value = "standard.colossus",
name = "Standard Colossus",
description = "320 GB RAM, 40 Cores, 10 - 1.6 TB Disk"
}
]
cpu_instances = [
{
value = "cpu.extra",
name = "CPU Extra-Large",
description = "16 GB RAM, 8 Cores, 10 - 800 GB Disk"
},
{
value = "cpu.huge",
name = "CPU Huge",
description = "32 GB RAM, 16 Cores, 10 - 800 GB Disk"
},
{
value = "cpu.mega",
name = "CPU Mega",
description = "64 GB RAM, 32 Cores, 10 - 800 GB Disk"
},
{
value = "cpu.titan",
name = "CPU Titan",
description = "128 GB RAM, 40 Cores, 0.1 - 1.6 TB Disk"
}
]
memory_instances = [
{
value = "memory.extra",
name = "Memory Extra-Large",
description = "16 GB RAM, 2 Cores, 10 - 800 GB Disk"
},
{
value = "memory.huge",
name = "Memory Huge",
description = "32 GB RAM, 4 Cores, 10 - 800 GB Disk"
},
{
value = "memory.mega",
name = "Memory Mega",
description = "64 GB RAM, 8 Cores, 10 - 800 GB Disk"
},
{
value = "memory.titan",
name = "Memory Titan",
description = "128 GB RAM, 12 Cores, 0.1 - 1.6 TB Disk"
}
]
storage_instances = [
{
value = "storage.extra",
name = "Storage Extra-Large",
description = "16 GB RAM, 4 Cores, 1 - 2 TB Disk"
},
{
value = "storage.huge",
name = "Storage Huge",
description = "32 GB RAM, 8 Cores, 2 - 3 TB Disk"
},
{
value = "storage.mega",
name = "Storage Mega",
description = "64 GB RAM, 12 Cores, 3 - 5 TB Disk"
},
{
value = "storage.titan",
name = "Storage Titan",
description = "128 GB RAM, 16 Cores, 5 - 10 TB Disk"
},
{
value = "storage.jumbo",
name = "Storage Jumbo",
description = "225 GB RAM, 24 Cores, 10 - 15 TB Disk"
}
]
gpu_instances = [
{
value = "gpu.small",
name = "GPU1 Small",
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
},
{
value = "gpu.medium",
name = "GPU1 Medium",
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
},
{
value = "gpu.large",
name = "GPU1 Large",
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
},
{
value = "gpu.huge",
name = "GPU1 Huge",
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
},
{
value = "gpu2.small",
name = "GPU2 Small",
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
},
{
value = "gpu2.medium",
name = "GPU2 Medium",
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
},
{
value = "gpu2.large",
name = "GPU2 Large",
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
},
{
value = "gpu2.huge",
name = "GPU2 Huge",
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
},
{
value = "gpu3.small",
name = "GPU3 Small",
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
},
{
value = "gpu3.medium",
name = "GPU3 Medium",
description = "120 GB RAM, 24 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
},
{
value = "gpu3.large",
name = "GPU3 Large",
description = "224 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
},
{
value = "gpu3.huge",
name = "GPU3 Huge",
description = "448 GB RAM, 96 Cores, 8 GPU, 0.1 - 1.6 TB Disk"
}
]
}
data "coder_parameter" "instance_type" {
name = "exoscale_instance_type"
display_name = var.display_name
description = var.description
default = var.default == "" ? null : var.default
mutable = var.mutable
dynamic "option" {
for_each = [for k, v in concat(
contains(var.type_category, "standard") ? local.standard_instances : [],
contains(var.type_category, "cpu") ? local.cpu_instances : [],
contains(var.type_category, "memory") ? local.memory_instances : [],
contains(var.type_category, "storage") ? local.storage_instances : [],
contains(var.type_category, "gpu") ? local.gpu_instances : []
) : v if !(contains(var.exclude, v.value))]
content {
name = try(var.custom_names[option.value.value], option.value.name)
description = try(var.custom_descriptions[option.value.value], option.value.description)
value = option.value.value
}
}
}
output "value" {
value = data.coder_parameter.instance_type.value
}

View File

@@ -1,98 +0,0 @@
---
display_name: exoscale-zone
description: A parameter with human zone names and icons
icon: ../.icons/exoscale.svg
maintainer_github: WhizUs
verified: false
tags: [helper, parameter, zones, regions, exoscale]
---
# exoscale-zone
A parameter with all Exoscale zones. This allows developers to select
the zone closest to them.
Customize the preselected parameter value:
```tf
module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.2"
default = "ch-dk-2"
}
data "exoscale_compute_template" "my_template" {
zone = module.exoscale-zone.value
name = "Linux Ubuntu 22.04 LTS 64-bit"
}
resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value
# ...
}
```
![Exoscale Zones](../.images/exoscale-zones.png)
## Examples
### Customize zones
Change the display name and icon for a zone using the corresponding maps:
```tf
module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.2"
default = "at-vie-1"
custom_names = {
"at-vie-1" : "Home Vienna"
}
custom_icons = {
"at-vie-1" : "/emojis/1f3e0.png"
}
}
data "exoscale_compute_template" "my_template" {
zone = module.exoscale-zone.value
name = "Linux Ubuntu 22.04 LTS 64-bit"
}
resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value
# ...
}
```
![Exoscale Custom](../.images/exoscale-custom.png)
### Exclude regions
Hide the Switzerland zones Geneva and Zurich
```tf
module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.2"
exclude = ["ch-gva-2", "ch-dk-2"]
}
data "exoscale_compute_template" "my_template" {
zone = module.exoscale-zone.value
name = "Linux Ubuntu 22.04 LTS 64-bit"
}
resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value
# ...
}
```
![Exoscale Exclude](../.images/exoscale-exclude.png)
## Related templates
An exoscale sample template will be delivered soon.

View File

@@ -1,25 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("exoscale-zone", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
default: "at-vie-1",
});
expect(state.outputs.value.value).toBe("at-vie-1");
});
});

View File

@@ -1,110 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "display_name" {
default = "Exoscale Region"
description = "The display name of the parameter."
type = string
}
variable "description" {
default = "The region to deploy workspace infrastructure."
description = "The description of the parameter."
type = string
}
variable "default" {
default = ""
description = "The default region to use if no region is specified."
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for region IDs."
type = map(string)
}
variable "custom_icons" {
default = {}
description = "A map of custom icons for region IDs."
type = map(string)
}
variable "exclude" {
default = []
description = "A list of region IDs to exclude."
type = list(string)
}
locals {
# This is a static list because the zones don't change _that_
# frequently and including the `exoscale_zones` data source requires
# the provider, which requires a zone.
# https://www.exoscale.com/datacenters/
zones = {
"de-fra-1" = {
name = "Frankfurt - Germany"
icon = "/emojis/1f1e9-1f1ea.png"
}
"at-vie-1" = {
name = "Vienna 1 - Austria"
icon = "/emojis/1f1e6-1f1f9.png"
}
"at-vie-2" = {
name = "Vienna 2 - Austria"
icon = "/emojis/1f1e6-1f1f9.png"
}
"ch-gva-2" = {
name = "Geneva - Switzerland"
icon = "/emojis/1f1e8-1f1ed.png"
}
"ch-dk-2" = {
name = "Zurich - Switzerland"
icon = "/emojis/1f1e8-1f1ed.png"
}
"bg-sof-1" = {
name = "Sofia - Bulgaria"
icon = "/emojis/1f1e7-1f1ec.png"
}
"de-muc-1" = {
name = "Munich - Germany"
icon = "/emojis/1f1e9-1f1ea.png"
}
}
}
data "coder_parameter" "zone" {
name = "exoscale_zone"
display_name = var.display_name
description = var.description
default = var.default == "" ? null : var.default
mutable = var.mutable
dynamic "option" {
for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) }
content {
name = try(var.custom_names[option.key], option.value.name)
icon = try(var.custom_icons[option.key], option.value.icon)
value = option.key
}
}
}
output "value" {
value = data.coder_parameter.zone.value
}

View File

@@ -11,10 +11,9 @@ tags: [helper, filebrowser]
A file browser for your workspace. A file browser for your workspace.
```tf ```hcl
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "https://registry.coder.com/modules/filebrowser"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -25,10 +24,9 @@ module "filebrowser" {
### Serve a specific directory ### Serve a specific directory
```tf ```hcl
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "https://registry.coder.com/modules/filebrowser"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/project" folder = "/home/coder/project"
} }
@@ -36,10 +34,9 @@ module "filebrowser" {
### Specify location of `filebrowser.db` ### Specify location of `filebrowser.db`
```tf ```hcl
module "filebrowser" { module "filebrowser" {
source = "registry.coder.com/modules/filebrowser/coder" source = "https://registry.coder.com/modules/filebrowser"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
database_path = ".config/filebrowser.db" database_path = ".config/filebrowser.db"
} }

View File

@@ -33,7 +33,7 @@ describe("filebrowser", async () => {
expect(output.stdout).toEqual([ expect(output.stdout).toEqual([
"\u001b[0;1mInstalling filebrowser ", "\u001b[0;1mInstalling filebrowser ",
"", "",
"🥳 Installation complete! ", "🥳 Installation comlete! ",
"", "",
"👷 Starting filebrowser in background... ", "👷 Starting filebrowser in background... ",
"", "",
@@ -55,7 +55,7 @@ describe("filebrowser", async () => {
expect(output.stdout).toEqual([ expect(output.stdout).toEqual([
"\u001b[0;1mInstalling filebrowser ", "\u001b[0;1mInstalling filebrowser ",
"", "",
"🥳 Installation complete! ", "🥳 Installation comlete! ",
"", "",
"👷 Starting filebrowser in background... ", "👷 Starting filebrowser in background... ",
"", "",
@@ -77,7 +77,7 @@ describe("filebrowser", async () => {
expect(output.stdout).toEqual([ expect(output.stdout).toEqual([
"\u001B[0;1mInstalling filebrowser ", "\u001B[0;1mInstalling filebrowser ",
"", "",
"🥳 Installation complete! ", "🥳 Installation comlete! ",
"", "",
"👷 Starting filebrowser in background... ", "👷 Starting filebrowser in background... ",
"", "",

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.12"
} }
} }
} }
@@ -43,21 +43,6 @@ variable "folder" {
default = "~" default = "~"
} }
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" "filebrowser" { resource "coder_script" "filebrowser" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "File Browser" display_name = "File Browser"
@@ -79,6 +64,5 @@ resource "coder_app" "filebrowser" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
subdomain = true subdomain = true
share = var.share share = "owner"
order = var.order
} }

View File

@@ -5,7 +5,7 @@ printf "$${BOLD}Installing filebrowser \n\n"
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
printf "🥳 Installation complete! \n\n" printf "🥳 Installation comlete! \n\n"
printf "👷 Starting filebrowser in background... \n\n" printf "👷 Starting filebrowser in background... \n\n"
@@ -21,6 +21,6 @@ printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n" printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} > ${LOG_PATH} 2>&1 & filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} >${LOG_PATH} 2>&1 &
printf "📝 Logs at ${LOG_PATH} \n\n" printf "📝 Logs at ${LOG_PATH} \n\n"

View File

@@ -13,10 +13,9 @@ This module adds Fly.io regions to your Coder template. Regions can be whitelist
We can use the simplest format here, only adding a default selection as the `atl` region. We can use the simplest format here, only adding a default selection as the `atl` region.
```tf ```hcl
module "fly-region" { module "fly-region" {
source = "registry.coder.com/modules/fly-region/coder" source = "https://registry.coder.com/modules/fly-region"
version = "1.0.2"
default = "atl" default = "atl"
} }
``` ```
@@ -29,10 +28,9 @@ module "fly-region" {
The regions argument can be used to display only the desired regions in the Coder parameter. The regions argument can be used to display only the desired regions in the Coder parameter.
```tf ```hcl
module "fly-region" { module "fly-region" {
source = "registry.coder.com/modules/fly-region/coder" source = "https://registry.coder.com/modules/fly-region"
version = "1.0.2"
default = "ams" default = "ams"
regions = ["ams", "arn", "atl"] regions = ["ams", "arn", "atl"]
} }
@@ -44,16 +42,13 @@ module "fly-region" {
Set custom icons and names with their respective maps. Set custom icons and names with their respective maps.
```tf ```hcl
module "fly-region" { module "fly-region" {
source = "registry.coder.com/modules/fly-region/coder" source = "https://registry.coder.com/modules/fly-region"
version = "1.0.2"
default = "ams" default = "ams"
custom_icons = { custom_icons = {
"ams" = "/emojis/1f90e.png" "ams" = "/emojis/1f90e.png"
} }
custom_names = { custom_names = {
"ams" = "We love the Netherlands!" "ams" = "We love the Netherlands!"
} }

View File

@@ -11,10 +11,9 @@ tags: [gcp, regions, parameter, helper]
This module adds Google Cloud Platform regions to your Coder template. This module adds Google Cloud Platform regions to your Coder template.
```tf ```hcl
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "https://registry.coder.com/modules/gcp-region"
version = "1.0.2"
regions = ["us", "europe"] regions = ["us", "europe"]
} }
@@ -31,10 +30,9 @@ resource "google_compute_instance" "example" {
Note: setting `gpu_only = true` and using a default region without GPU support, the default will be set to `null`. Note: setting `gpu_only = true` and using a default region without GPU support, the default will be set to `null`.
```tf ```hcl
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "https://registry.coder.com/modules/gcp-region"
version = "1.0.2"
default = ["us-west1-a"] default = ["us-west1-a"]
regions = ["us-west1"] regions = ["us-west1"]
gpu_only = false gpu_only = false
@@ -47,10 +45,9 @@ resource "google_compute_instance" "example" {
### Add all zones in the Europe West region ### Add all zones in the Europe West region
```tf ```hcl
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "https://registry.coder.com/modules/gcp-region"
version = "1.0.2"
regions = ["europe-west"] regions = ["europe-west"]
single_zone_per_region = false single_zone_per_region = false
} }
@@ -60,12 +57,11 @@ resource "google_compute_instance" "example" {
} }
``` ```
### Add a single zone from each region in US and Europe that has GPUs ### Add a single zone from each region in US and Europe that laos has GPUs
```tf ```hcl
module "gcp_region" { module "gcp_region" {
source = "registry.coder.com/modules/gcp-region/coder" source = "https://registry.coder.com/modules/gcp-region"
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

@@ -9,44 +9,33 @@ tags: [git, helper]
# Git Clone # Git Clone
This module allows you to automatically clone a repository by URL and skip if it exists in the base directory provided. This module allows you to automatically clone a repository by URL and skip if it exists in the path provided.
```tf ```hcl
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "https://registry.coder.com/modules/git-clone"
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"
} }
``` ```
To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-providers), add the provider by ID to your template:
```hcl
data "coder_git_auth" "github" {
id = "github"
}
```
## Examples ## Examples
### Custom Path ### Custom Path
```tf ```hcl
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "https://registry.coder.com/modules/git-clone"
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" path = "~/projects/coder/coder"
}
```
### Git Authentication
To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-providers), add the provider by ID to your template:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
id = "github"
} }
``` ```

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.11"
} }
} }
} }
@@ -14,9 +14,9 @@ variable "url" {
type = string type = string
} }
variable "base_dir" { variable "path" {
default = "" default = ""
description = "The base directory to clone the repository. Defaults to \"$HOME\"." description = "The path to clone the repository. Defaults to \"$HOME/<basename of url>\"."
type = string type = string
} }
@@ -25,19 +25,10 @@ variable "agent_id" {
type = string type = string
} }
locals {
clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")])
}
output "repo_dir" {
value = local.clone_path
description = "Full path of cloned repo directory"
}
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 : var.path != "" ? var.path : join("/", ["~", basename(var.url)]),
REPO_URL : var.url, REPO_URL : var.url,
}) })
display_name = "Git Clone" display_name = "Git Clone"

View File

@@ -18,7 +18,7 @@ if [ -z "$CLONE_PATH" ]; then
fi fi
# Check if `git` is installed... # Check if `git` is installed...
if ! command -v git > /dev/null; then if ! command -v git >/dev/null; then
echo "Git is not installed!" echo "Git is not installed!"
exit 1 exit 1
fi fi

View File

@@ -1,25 +0,0 @@
---
display_name: Git commit signing
description: Configures Git to sign commits using your Coder SSH key
icon: ../.icons/git.svg
maintainer_github: phorcys420
verified: false
tags: [helper, git]
---
# git-commit-signing
This module downloads your SSH key from Coder and uses it to sign commits with Git.
It requires `curl` and `jq` to be installed inside your workspace.
Please observe that using the SSH key that's part of your Coder account for commit signing, means that in the event of a breach of your Coder account, or a malicious admin, someone could perform commit signing pretending to be you.
This module has a chance of conflicting with the user's dotfiles / the personalize module if one of those has configuration directives that overwrite this module's / each other's git configuration.
```tf
module "git-commit-signing" {
source = "registry.coder.com/modules/git-commit-signing/coder"
version = "1.0.3"
agent_id = coder_agent.example.id
}
```

View File

@@ -1,25 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
resource "coder_script" "git-commit-signing" {
display_name = "Git commit signing"
icon = "https://raw.githubusercontent.com/coder/modules/main/.icons/git.svg"
script = file("${path.module}/run.sh")
run_on_start = true
agent_id = var.agent_id
}

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env sh
if ! command -v git > /dev/null; then
echo "git is not installed"
exit 1
fi
if ! command -v curl > /dev/null; then
echo "curl is not installed"
exit 1
fi
if ! command -v jq > /dev/null; then
echo "jq is not installed"
exit 1
fi
mkdir -p ~/.ssh/git-commit-signing
echo "Downloading SSH key"
ssh_key=$(curl --request GET \
--url "${CODER_AGENT_URL}api/v2/workspaceagents/me/gitsshkey" \
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}" \
--silent --show-error)
jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
$ssh_key
EOF
jq --raw-output ".private_key" > ~/.ssh/git-commit-signing/coder << EOF
$ssh_key
EOF
chmod -R 600 ~/.ssh/git-commit-signing/coder
chmod -R 644 ~/.ssh/git-commit-signing/coder.pub
echo "Configuring git to use the SSH key"
git config --global gpg.format ssh
git config --global commit.gpgsign true
git config --global user.signingkey ~/.ssh/git-commit-signing/coder

View File

@@ -11,10 +11,9 @@ tags: [helper, git]
Runs a script that updates git credentials in the workspace to match the user's Coder credentials, optionally allowing to the developer to override the defaults. Runs a script that updates git credentials in the workspace to match the user's Coder credentials, optionally allowing to the developer to override the defaults.
```tf ```hcl
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "https://registry.coder.com/modules/git-config"
version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -25,10 +24,9 @@ TODO: Add screenshot
### Allow users to override both username and email ### Allow users to override both username and email
```tf ```hcl
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "https://registry.coder.com/modules/git-config"
version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
allow_email_change = true allow_email_change = true
} }
@@ -38,12 +36,13 @@ TODO: Add screenshot
## Disallowing users from overriding both username and email ## Disallowing users from overriding both username and email
```tf ```hcl
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "https://registry.coder.com/modules/git-config"
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
} }
``` ```
TODO: Add screenshot

43
git-config/main.test.ts Normal file
View File

@@ -0,0 +1,43 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("git-config", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("fails without git", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(1);
expect(output.stdout).toEqual([
"\u001B[0;1mChecking git-config!",
"Git is not installed!",
]);
});
it("runs with git", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"\u001B[0;1mChecking git-config!",
"git-config: No user.email found, setting to ",
"git-config: No user.name found, setting to default",
"",
"\u001B[0;1mgit-config: using email: ",
"\u001B[0;1mgit-config: using username: default",
]);
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.13" version = ">= 0.12"
} }
} }
} }
@@ -34,7 +34,7 @@ data "coder_parameter" "user_email" {
name = "user_email" name = "user_email"
type = "string" type = "string"
default = "" default = ""
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 username."
display_name = "Git config user.email" display_name = "Git config user.email"
mutable = true mutable = true
} }
@@ -44,31 +44,18 @@ data "coder_parameter" "username" {
name = "username" name = "username"
type = "string" type = "string"
default = "" default = ""
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 username."
display_name = "Full Name for Git config" display_name = "Git config user.name"
mutable = true mutable = true
} }
resource "coder_env" "git_author_name" { resource "coder_script" "git_config" {
agent_id = var.agent_id agent_id = var.agent_id
name = "GIT_AUTHOR_NAME" script = templatefile("${path.module}/run.sh", {
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner) GIT_USERNAME = try(data.coder_parameter.username[0].value, "") == "" ? data.coder_workspace.me.owner : try(data.coder_parameter.username[0].value, "")
} GIT_EMAIL = try(data.coder_parameter.user_email[0].value, "") == "" ? data.coder_workspace.me.owner_email : try(data.coder_parameter.user_email[0].value, "")
})
resource "coder_env" "git_commmiter_name" { display_name = "Git Config"
agent_id = var.agent_id icon = "/icon/git.svg"
name = "GIT_COMMITTER_NAME" run_on_start = true
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" {
agent_id = var.agent_id
name = "GIT_AUTHOR_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
}
resource "coder_env" "git_commmiter_email" {
agent_id = var.agent_id
name = "GIT_COMMITTER_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
} }

24
git-config/run.sh Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env sh
BOLD='\033[0;1m'
printf "$${BOLD}Checking git-config!\n"
# Check if git is installed
command -v git >/dev/null 2>&1 || {
echo "Git is not installed!"
exit 1
}
# Set git username and email if missing
if [ -z $(git config --get user.email) ]; then
printf "git-config: No user.email found, setting to ${GIT_EMAIL}\n"
git config --global user.email "${GIT_EMAIL}"
fi
if [ -z $(git config --get user.name) ]; then
printf "git-config: No user.name found, setting to ${GIT_USERNAME}\n"
git config --global user.name "${GIT_USERNAME}"
fi
printf "\n$${BOLD}git-config: using email: $(git config --get user.email)\n"
printf "$${BOLD}git-config: using username: $(git config --get user.name)\n\n"

View File

@@ -1,80 +0,0 @@
---
display_name: "HCP Vault Secrets"
description: "Fetch secrets from HCP Vault"
icon: ../.icons/vault.svg
maintainer_github: coder
partner_github: hashicorp
verified: true
tags: [helper, integration, vault, hashicorp, hvs]
---
# HCP Vault Secrets
This module lets you fetch all or selective secrets from a [HCP Vault Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) app into your [Coder](https://coder.com) workspaces. It makes use of the [`hcp_vault_secrets_app`](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/data-sources/vault_secrets_app) data source from the [HCP provider](https://registry.terraform.io/providers/hashicorp/hcp/latest).
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
app_name = "demo-app"
project_id = "aaa-bbb-ccc"
}
```
## Configuration
To configure the HCP Vault Secrets module, follow these steps,
1. [Create secrets in HCP Vault Secrets](https://developer.hashicorp.com/vault/tutorials/hcp-vault-secrets-get-started/hcp-vault-secrets-create-secret)
2. Create an HCP Service Principal from the HCP Vault Secrets app in the HCP console. This will give you the `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` that you need to authenticate with HCP Vault Secrets.
![HCP vault secrets credentials](../.images/hcp-vault-secrets-credentials.png)
3. Set `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` variables on the coder provisioner (recommended) or supply them as input to the module.
4. Set the `project_id`. This is the ID of the project where the HCP Vault Secrets app is running.
> See the [HCP Vault Secrets documentation](https://developer.hashicorp.com/hcp/docs/vault-secrets) for more information.
## Fetch All Secrets
To fetch all secrets from the HCP Vault Secrets app, skip the `secrets` input.
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
app_name = "demo-app"
project_id = "aaa-bbb-ccc"
}
```
## Fetch Selective Secrets
To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` input.
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
app_name = "demo-app"
project_id = "aaa-bbb-ccc"
secrets = ["MY_SECRET_1", "MY_SECRET_2"]
}
```
## Set Client ID and Client Secret as Inputs
Set `client_id` and `client_secret` as module inputs.
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
app_name = "demo-app"
project_id = "aaa-bbb-ccc"
client_id = "HCP_CLIENT_ID"
client_secret = "HCP_CLIENT_SECRET"
}
```

View File

@@ -1,73 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
}
hcp = {
source = "hashicorp/hcp"
version = ">= 0.82.0"
}
}
}
provider "hcp" {
client_id = var.client_id
client_secret = var.client_secret
project_id = var.project_id
}
provider "coder" {}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "project_id" {
type = string
description = "The ID of the HCP project."
}
variable "client_id" {
type = string
description = <<-EOF
The client ID for the HCP Vault Secrets service principal. (Optional if HCP_CLIENT_ID is set as an environment variable.)
EOF
default = null
sensitive = true
}
variable "client_secret" {
type = string
description = <<-EOF
The client secret for the HCP Vault Secrets service principal. (Optional if HCP_CLIENT_SECRET is set as an environment variable.)
EOF
default = null
sensitive = true
}
variable "app_name" {
type = string
description = "The name of the secrets app in HCP Vault Secrets"
}
variable "secrets" {
type = list(string)
description = "The names of the secrets to retrieve from HCP Vault Secrets"
default = null
}
data "hcp_vault_secrets_app" "secrets" {
app_name = var.app_name
}
resource "coder_env" "hvs_secrets" {
# https://support.hashicorp.com/hc/en-us/articles/4538432032787-Variable-has-a-sensitive-value-and-cannot-be-used-as-for-each-arguments
for_each = var.secrets != null ? toset(var.secrets) : nonsensitive(toset(keys(data.hcp_vault_secrets_app.secrets.secrets)))
agent_id = var.agent_id
name = each.key
value = data.hcp_vault_secrets_app.secrets.secrets[each.key]
}

View File

@@ -11,15 +11,13 @@ tags: [ide, jetbrains, helper, parameter]
This module adds a JetBrains Gateway Button to open any workspace with a single click. This module adds a JetBrains Gateway Button to open any workspace with a single click.
```tf ```hcl
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "https://registry.coder.com/modules/jetbrains-gateway"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example" agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS", "IU", "PY", "PS", "CL", "RM", "RD"] jetbrains_ides = ["GO", "WS", "IU", "IC", "PY", "PC", "PS", "CL", "RM", "DB", "RD"]
default = "PY"
} }
``` ```
@@ -29,10 +27,9 @@ module "jetbrains_gateway" {
### Add GoLand and WebStorm with the default set to GoLand ### Add GoLand and WebStorm with the default set to GoLand
```tf ```hcl
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "https://registry.coder.com/modules/jetbrains-gateway"
version = "1.0.8"
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"
@@ -48,8 +45,11 @@ This module and JetBrains Gateway support the following JetBrains IDEs:
- GoLand (`GO`) - GoLand (`GO`)
- WebStorm (`WS`) - WebStorm (`WS`)
- IntelliJ IDEA Ultimate (`IU`) - IntelliJ IDEA Ultimate (`IU`)
- IntelliJ IDEA Community (`IC`)
- PyCharm Professional (`PY`) - PyCharm Professional (`PY`)
- PyCharm Community (`PC`)
- PhpStorm (`PS`) - PhpStorm (`PS`)
- CLion (`CL`) - CLion (`CL`)
- RubyMine (`RM`) - RubyMine (`RM`)
- DataGrip (`DB`)
- Rider (`RD`) - Rider (`RD`)

View File

@@ -2,6 +2,7 @@ import { it, expect, describe } from "bun:test";
import { import {
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
executeScriptInContainer,
runTerraformApply, runTerraformApply,
} from "../test"; } from "../test";
@@ -10,17 +11,20 @@ describe("jetbrains-gateway", async () => {
await testRequiredVariables(import.meta.dir, { await testRequiredVariables(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
agent_name: "foo", agent_name: "bar",
folder: "/home/foo", folder: "/baz/",
jetbrains_ides: '["IU", "IC", "PY"]',
}); });
it("default to first ide", async () => { it("default to first ide", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
agent_name: "foo", agent_name: "bar",
folder: "/home/foo", folder: "/baz/",
jetbrains_ides: '["IU", "GO", "PY"]', jetbrains_ides: '["IU", "IC", "PY"]',
}); });
expect(state.outputs.identifier.value).toBe("IU"); expect(state.outputs.jetbrains_ides.value).toBe(
'["IU","232.9921.47","https://download.jetbrains.com/idea/ideaIU-2023.2.2.tar.gz"]',
);
}); });
}); });

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.11"
} }
} }
} }
@@ -16,16 +16,12 @@ variable "agent_id" {
variable "agent_name" { variable "agent_name" {
type = string type = string
description = "Agent name." description = "The name of a Coder agent."
} }
variable "folder" { variable "folder" {
type = string type = string
description = "The directory to open in the IDE. e.g. /home/coder/project" description = "The directory to open in the IDE. e.g. /home/coder/project"
validation {
condition = can(regex("^(?:/[^/]+)+$", var.folder))
error_message = "The folder must be a full path and must not start with a ~."
}
} }
variable "default" { variable "default" {
@@ -34,73 +30,16 @@ variable "default" {
description = "Default IDE" description = "Default IDE"
} }
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
}
variable "jetbrains_ide_versions" {
type = map(object({
build_number = string
version = string
}))
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "233.14808.21"
version = "2023.3.5"
}
"PS" = {
build_number = "233.14808.18"
version = "2023.3.5"
}
"WS" = {
build_number = "233.14475.40"
version = "2023.3.4"
}
"PY" = {
build_number = "233.14475.56"
version = "2023.3.4"
}
"CL" = {
build_number = "233.14475.31"
version = "2023.3.4"
}
"GO" = {
build_number = "233.14808.20"
version = "2023.3.5"
}
"RM" = {
build_number = "233.14808.14"
version = "2023.3.5"
}
"RD" = {
build_number = "233.14475.66"
version = "2023.3.4"
}
}
validation {
condition = (
alltrue([
for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
])
)
error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
}
}
variable "jetbrains_ides" { variable "jetbrains_ides" {
type = list(string) type = list(string)
description = "The list of IDE product codes." description = "The list of IDE product codes."
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
validation { validation {
condition = ( condition = (
alltrue([ alltrue([
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code) for code in var.jetbrains_ides : contains(["IU", "IC", "PS", "WS", "PY", "PC", "CL", "GO", "DB", "RD", "RM"], code)
]) ])
) )
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}." error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are: IU, IC, PS, WS, PY, PC, CL, GO, DB, RD, RM."
} }
# check if the list is empty # check if the list is empty
validation { validation {
@@ -119,76 +58,76 @@ locals {
"GO" = { "GO" = {
icon = "/icon/goland.svg", icon = "/icon/goland.svg",
name = "GoLand", name = "GoLand",
identifier = "GO", value = jsonencode(["GO", "232.9921.53", "https://download.jetbrains.com/go/goland-2023.2.2.tar.gz"])
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"
}, },
"WS" = { "WS" = {
icon = "/icon/webstorm.svg", icon = "/icon/webstorm.svg",
name = "WebStorm", name = "WebStorm",
identifier = "WS", value = jsonencode(["WS", "232.9921.42", "https://download.jetbrains.com/webstorm/WebStorm-2023.2.2.tar.gz"])
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"
}, },
"IU" = { "IU" = {
icon = "/icon/intellij.svg", icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Ultimate", name = "IntelliJ IDEA Ultimate",
identifier = "IU", value = jsonencode(["IU", "232.9921.47", "https://download.jetbrains.com/idea/ideaIU-2023.2.2.tar.gz"])
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" "IC" = {
icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Community",
value = jsonencode(["IC", "232.9921.47", "https://download.jetbrains.com/idea/ideaIC-2023.2.2.tar.gz"])
}, },
"PY" = { "PY" = {
icon = "/icon/pycharm.svg", icon = "/icon/pycharm.svg",
name = "PyCharm Professional", name = "PyCharm Professional",
identifier = "PY", value = jsonencode(["PY", "232.9559.58", "https://download.jetbrains.com/python/pycharm-professional-2023.2.1.tar.gz"])
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"
}, },
"PC" = {
icon = "/icon/pycharm.svg",
name = "PyCharm Community",
value = jsonencode(["PC", "232.9559.58", "https://download.jetbrains.com/python/pycharm-community-2023.2.1.tar.gz"])
},
"RD" = {
icon = "/icon/rider.svg",
name = "Rider",
value = jsonencode(["RD", "232.9559.61", "https://download.jetbrains.com/rider/JetBrains.Rider-2023.2.1.tar.gz"])
}
"CL" = { "CL" = {
icon = "/icon/clion.svg", icon = "/icon/clion.svg",
name = "CLion", name = "CLion",
identifier = "CL", value = jsonencode(["CL", "232.9921.42", "https://download.jetbrains.com/cpp/CLion-2023.2.2.tar.gz"])
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" "DB" = {
icon = "/icon/datagrip.svg",
name = "DataGrip",
value = jsonencode(["DB", "232.9559.28", "https://download.jetbrains.com/datagrip/datagrip-2023.2.1.tar.gz"])
}, },
"PS" = { "PS" = {
icon = "/icon/phpstorm.svg", icon = "/icon/phpstorm.svg",
name = "PhpStorm", name = "PhpStorm",
identifier = "PS", value = jsonencode(["PS", "232.9559.64", "https://download.jetbrains.com/webide/PhpStorm-2023.2.1.tar.gz"])
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"
}, },
"RM" = { "RM" = {
icon = "/icon/rubymine.svg", icon = "/icon/rubymine.svg",
name = "RubyMine", name = "RubyMine",
identifier = "RM", value = jsonencode(["RM", "232.9921.48", "https://download.jetbrains.com/ruby/RubyMine-2023.2.2.tar.gz"])
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"
}
"RD" = {
icon = "/icon/rider.svg",
name = "Rider",
identifier = "RD",
build_number = var.jetbrains_ide_versions["RD"].build_number,
download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
} }
} }
} }
data "coder_parameter" "jetbrains_ide" { data "coder_parameter" "jetbrains_ide" {
type = "string" type = "list(string)"
name = "jetbrains_ide" name = "jetbrains_ide"
display_name = "JetBrains IDE" display_name = "JetBrains IDE"
icon = "/icon/gateway.svg" icon = "/icon/gateway.svg"
mutable = true mutable = true
default = var.default == "" ? var.jetbrains_ides[0] : var.default # check if default is in the jet_brains_ides list and if it is not empty or null otherwise set it to null
default = var.default != null && var.default != "" && contains(var.jetbrains_ides, var.default) ? local.jetbrains_ides[var.default].value : local.jetbrains_ides[var.jetbrains_ides[0]].value
dynamic "option" { dynamic "option" {
for_each = var.jetbrains_ides for_each = { for key, value in local.jetbrains_ides : key => value if contains(var.jetbrains_ides, key) }
content { content {
icon = lookup(local.jetbrains_ides, option.value).icon icon = option.value.icon
name = lookup(local.jetbrains_ides, option.value).name name = option.value.name
value = lookup(local.jetbrains_ides, option.value).identifier value = option.value.value
} }
} }
} }
@@ -197,11 +136,10 @@ data "coder_workspace" "me" {}
resource "coder_app" "gateway" { resource "coder_app" "gateway" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].name
slug = "gateway" slug = "gateway"
display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE") icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon
icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg")
external = true external = true
order = var.order
url = join("", [ url = join("", [
"jetbrains-gateway://connect#type=coder&workspace=", "jetbrains-gateway://connect#type=coder&workspace=",
data.coder_workspace.me.name, data.coder_workspace.me.name,
@@ -214,38 +152,14 @@ resource "coder_app" "gateway" {
"&token=", "&token=",
"$SESSION_TOKEN", "$SESSION_TOKEN",
"&ide_product_code=", "&ide_product_code=",
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier, jsondecode(data.coder_parameter.jetbrains_ide.value)[0],
"&ide_build_number=", "&ide_build_number=",
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number, jsondecode(data.coder_parameter.jetbrains_ide.value)[1],
"&ide_download_link=", "&ide_download_link=",
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link jsondecode(data.coder_parameter.jetbrains_ide.value)[2],
]) ])
} }
output "identifier" { output "jetbrains_ides" {
value = data.coder_parameter.jetbrains_ide.value value = data.coder_parameter.jetbrains_ide.value
} }
output "name" {
value = coder_app.gateway.display_name
}
output "icon" {
value = coder_app.gateway.icon
}
output "download_link" {
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link
}
output "build_number" {
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number
}
output "version" {
value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
}
output "url" {
value = coder_app.gateway.url
}

View File

@@ -1,103 +0,0 @@
---
display_name: JFrog (OAuth)
description: Install the JF CLI and authenticate with Artifactory using OAuth.
icon: ../.icons/jfrog.svg
maintainer_github: coder
partner_github: jfrog
verified: true
tags: [integration, jfrog]
---
# JFrog
Install the JF CLI and authenticate package managers with Artifactory using OAuth configured via the Coder [`external-auth`](https://coder.com/docs/v2/latest/admin/external-auth) feature.
![JFrog OAuth](../.images/jfrog-oauth.png)
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
package_managers = {
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
> Note
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
## Prerequisites
This module is usable by JFrog self-hosted (on-premises) Artifactory as it requires configuring a custom integration. This integration benefits from Coder's [external-auth](https://coder.com/docs/v2/latest/admin/external-auth) feature and allows each user to authenticate with Artifactory using an OAuth flow and issues user-scoped tokens to each user. For configuration instructions, see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-oauth) on the Coder documentation.
## Examples
Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username.
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
package_managers = {
"pypi" : "pypi"
}
}
```
You should now be able to install packages from Artifactory using both the `jf pip` and `pip` command.
```shell
jf pip install requests
```
```shell
pip install requests
```
### Configure code-server with JFrog extension
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = {
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
### Using the access token in other terraform resources
JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```tf
provider "docker" {
# ...
registry_auth {
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username
password = module.jfrog.access_token
}
}
```
> Here `REPO_KEY` is the name of docker repository in Artifactory.

View File

@@ -1,19 +0,0 @@
import { serve } from "bun";
import { describe } from "bun:test";
import {
createJSONResponse,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("jfrog-oauth", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: "http://localhost:8081",
package_managers: "{}",
});
});
//TODO add more tests

View File

@@ -1,138 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
}
}
}
variable "jfrog_url" {
type = string
description = "JFrog instance URL. e.g. https://myartifactory.jfrog.io"
# ensue the URL is HTTPS or HTTP
validation {
condition = can(regex("^(https|http)://", var.jfrog_url))
error_message = "jfrog_url must be a valid URL starting with either 'https://' or 'http://'"
}
}
variable "jfrog_server_id" {
type = string
description = "The server ID of the JFrog instance for JFrog CLI configuration"
default = "0"
}
variable "username_field" {
type = string
description = "The field to use for the artifactory username. i.e. Coder username or email."
default = "username"
validation {
condition = can(regex("^(email|username)$", var.username_field))
error_message = "username_field must be either 'email' or 'username'"
}
}
variable "external_auth_id" {
type = string
description = "JFrog external auth ID. Default: 'jfrog'"
default = "jfrog"
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "configure_code_server" {
type = bool
description = "Set to true to configure code-server to use JFrog."
default = false
}
variable "package_managers" {
type = map(string)
description = <<EOF
A map of package manager names to their respective artifactory repositories.
For example:
{
"npm": "YOUR_NPM_REPO_KEY",
"go": "YOUR_GO_REPO_KEY",
"pypi": "YOUR_PYPI_REPO_KEY",
"docker": "YOUR_DOCKER_REPO_KEY"
}
EOF
}
locals {
# The username field to use for artifactory
username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
jfrog_host = replace(var.jfrog_url, "https://", "")
}
data "coder_workspace" "me" {}
data "coder_external_auth" "jfrog" {
id = var.external_auth_id
}
resource "coder_script" "jfrog" {
agent_id = var.agent_id
display_name = "jfrog"
icon = "/icon/jfrog.svg"
script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
REPOSITORY_GO : lookup(var.package_managers, "go", ""),
REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""),
REPOSITORY_DOCKER : lookup(var.package_managers, "docker", ""),
})
run_on_start = true
}
resource "coder_env" "jfrog_ide_url" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_URL"
value = var.jfrog_url
}
resource "coder_env" "jfrog_ide_access_token" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_ACCESS_TOKEN"
value = data.coder_external_auth.jfrog.access_token
}
resource "coder_env" "jfrog_ide_store_connection" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_STORE_CONNECTION"
value = true
}
resource "coder_env" "goproxy" {
count = lookup(var.package_managers, "go", "") == "" ? 0 : 1
agent_id = var.agent_id
name = "GOPROXY"
value = "https://${local.username}:${data.coder_external_auth.jfrog.access_token}@${local.jfrog_host}/artifactory/api/go/${lookup(var.package_managers, "go", "")}"
}
output "access_token" {
description = "value of the JFrog access token"
value = data.coder_external_auth.jfrog.access_token
sensitive = true
}
output "username" {
description = "value of the JFrog username"
value = local.username
}

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
# check if JFrog CLI is already installed
if command -v jf > /dev/null 2>&1; then
echo "✅ JFrog CLI is already installed, skipping installation."
else
echo "📦 Installing JFrog CLI..."
curl -fL https://install-cli.jfrog.io | sudo sh
sudo chmod 755 /usr/local/bin/jf
fi
# The jf CLI checks $CI when determining whether to use interactive
# flows.
export CI=true
# Authenticate JFrog CLI with Artifactory.
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
# Set the configured server as the default.
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then
echo "🤔 no npm repository is set, skipping npm configuration."
echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
cat << EOF > ~/.npmrc
email=${ARTIFACTORY_EMAIL}
registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
EOF
echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
fi
# Configure the `pip` to use the Artifactory "python" repository.
if [ -z "${REPOSITORY_PYPI}" ]; then
echo "🤔 no pypi repository is set, skipping pip configuration."
echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
else
echo "📦 Configuring pip..."
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
mkdir -p ~/.pip
cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple
EOF
fi
# Configure Artifactory "go" repository.
if [ -z "${REPOSITORY_GO}" ]; then
echo "🤔 no go repository is set, skipping go configuration."
echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
else
echo "🐹 Configuring go..."
jf goc --global --repo-resolve "${REPOSITORY_GO}"
fi
echo "🥳 Configuration complete!"
# Configure the JFrog CLI to use the Artifactory "docker" repository.
if [ -z "${REPOSITORY_DOCKER}" ]; then
echo "🤔 no docker repository is set, skipping docker configuration."
echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
else
if command -v docker > /dev/null 2>&1; then
echo "🔑 Configuring 🐳 docker credentials..."
mkdir -p ~/.docker
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
else
echo "🤔 no docker is installed, skipping docker configuration."
fi
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
counter=0
if [ $counter -eq 60 ]; then
echo "Timed out waiting for /tmp/code-server/bin/code-server to be installed."
exit 1
fi
echo "Waiting for /tmp/code-server/bin/code-server to be installed..."
sleep 1
((counter++))
done
echo "📦 Installing JFrog extension..."
/tmp/code-server/bin/code-server --install-extension jfrog.jfrog-vscode-extension
echo "🥳 JFrog extension installed!"
else
echo "🤔 Skipping JFrog extension installation. Set configure_code_server to true to install the JFrog extension."
fi
# Configure the JFrog CLI completion
echo "📦 Configuring JFrog CLI completion..."
# Get the user's shell
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
# Generate the completion script
jf completion $SHELLNAME --install
# Add the completion script to the user's shell profile
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
echo "" >> ~/.bashrc
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.bashrc
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
echo "# END: jf CLI shell completion" >> ~/.bashrc
else
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
fi
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
echo "" >> ~/.zshrc
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.zshrc
echo "autoload -Uz compinit" >> ~/.zshrc
echo "compinit" >> ~/.zshrc
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
echo "# END: jf CLI shell completion" >> ~/.zshrc
else
echo "🥳 ~/.zshrc already contains jf CLI shell completion configuration, skipping."
fi
else
echo "🤔 ~/.bashrc or ~/.zshrc does not exist, skipping jf CLI shell completion configuration."
fi

View File

@@ -1,106 +0,0 @@
---
display_name: JFrog (Token)
description: Install the JF CLI and authenticate with Artifactory using Artifactory terraform provider.
icon: ../.icons/jfrog.svg
maintainer_github: coder
partner_github: jfrog
verified: true
tags: [integration, jfrog]
---
# JFrog
Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider.
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
package_managers = {
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
For detailed instructions, please see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-token) on the Coder documentation.
> Note
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
![JFrog](../.images/jfrog.png)
## Examples
### Configure npm, go, and pypi to use Artifactory local repositories
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
"npm" : "npm-local",
"go" : "go-local",
"pypi" : "pypi-local"
}
}
```
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip` commands.
```shell
jf npm install prettier
jf go get github.com/golang/example/hello
jf pip install requests
```
```shell
npm install prettier
go get github.com/golang/example/hello
pip install requests
```
### Configure code-server with JFrog extension
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = {
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
### Using the access token in other terraform resources
JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```tf
provider "docker" {
# ...
registry_auth {
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username
password = module.jfrog.access_token
}
}
```
> Here `REPO_KEY` is the name of docker repository in Artifactory.

View File

@@ -1,171 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
}
artifactory = {
source = "registry.terraform.io/jfrog/artifactory"
version = "~> 10.0.2"
}
}
}
variable "jfrog_url" {
type = string
description = "JFrog instance URL. e.g. https://myartifactory.jfrog.io"
# ensue the URL is HTTPS or HTTP
validation {
condition = can(regex("^(https|http)://", var.jfrog_url))
error_message = "jfrog_url must be a valid URL starting with either 'https://' or 'http://'"
}
}
variable "jfrog_server_id" {
type = string
description = "The server ID of the JFrog instance for JFrog CLI configuration"
default = "0"
}
variable "artifactory_access_token" {
type = string
description = "The admin-level access token to use for JFrog."
}
variable "check_license" {
type = bool
description = "Toggle for pre-flight checking of Artifactory license. Default to `true`."
default = true
}
variable "refreshable" {
type = bool
description = "Is this token refreshable? Default is `false`."
default = false
}
variable "expires_in" {
type = number
description = "The amount of time, in seconds, it would take for the token to expire."
default = null
}
variable "username_field" {
type = string
description = "The field to use for the artifactory username. Default `username`."
default = "username"
validation {
condition = can(regex("^(email|username)$", var.username_field))
error_message = "username_field must be either 'email' or 'username'"
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "configure_code_server" {
type = bool
description = "Set to true to configure code-server to use JFrog."
default = false
}
variable "package_managers" {
type = map(string)
description = <<EOF
A map of package manager names to their respective artifactory repositories.
For example:
{
"npm": "YOUR_NPM_REPO_KEY",
"go": "YOUR_GO_REPO_KEY",
"pypi": "YOUR_PYPI_REPO_KEY",
"docker": "YOUR_DOCKER_REPO_KEY"
}
EOF
}
locals {
# The username field to use for artifactory
username = var.username_field == "email" ? data.coder_workspace.me.owner_email : data.coder_workspace.me.owner
jfrog_host = replace(var.jfrog_url, "https://", "")
}
# Configure the Artifactory provider
provider "artifactory" {
url = join("/", [var.jfrog_url, "artifactory"])
access_token = var.artifactory_access_token
check_license = var.check_license
}
resource "artifactory_scoped_token" "me" {
# This is hacky, but on terraform plan the data source gives empty strings,
# which fails validation.
username = length(local.username) > 0 ? local.username : "dummy"
scopes = ["applied-permissions/user"]
refreshable = var.refreshable
expires_in = var.expires_in
}
data "coder_workspace" "me" {}
resource "coder_script" "jfrog" {
agent_id = var.agent_id
display_name = "jfrog"
icon = "/icon/jfrog.svg"
script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
REPOSITORY_GO : lookup(var.package_managers, "go", ""),
REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""),
REPOSITORY_DOCKER : lookup(var.package_managers, "docker", ""),
})
run_on_start = true
}
resource "coder_env" "jfrog_ide_url" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_URL"
value = var.jfrog_url
}
resource "coder_env" "jfrog_ide_access_token" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_ACCESS_TOKEN"
value = artifactory_scoped_token.me.access_token
}
resource "coder_env" "jfrog_ide_store_connection" {
count = var.configure_code_server ? 1 : 0
agent_id = var.agent_id
name = "JFROG_IDE_STORE_CONNECTION"
value = true
}
resource "coder_env" "goproxy" {
count = lookup(var.package_managers, "go", "") == "" ? 0 : 1
agent_id = var.agent_id
name = "GOPROXY"
value = "https://${local.username}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/go/${lookup(var.package_managers, "go", "")}"
}
output "access_token" {
description = "value of the JFrog access token"
value = artifactory_scoped_token.me.access_token
sensitive = true
}
output "username" {
description = "value of the JFrog username"
value = local.username
}

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env bash
BOLD='\033[0;1m'
# check if JFrog CLI is already installed
if command -v jf > /dev/null 2>&1; then
echo "✅ JFrog CLI is already installed, skipping installation."
else
echo "📦 Installing JFrog CLI..."
curl -fL https://install-cli.jfrog.io | sudo sh
sudo chmod 755 /usr/local/bin/jf
fi
# The jf CLI checks $CI when determining whether to use interactive
# flows.
export CI=true
# Authenticate JFrog CLI with Artifactory.
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
# Set the configured server as the default.
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then
echo "🤔 no npm repository is set, skipping npm configuration."
echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
cat << EOF > ~/.npmrc
email=${ARTIFACTORY_EMAIL}
registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
EOF
echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
fi
# Configure the `pip` to use the Artifactory "python" repository.
if [ -z "${REPOSITORY_PYPI}" ]; then
echo "🤔 no pypi repository is set, skipping pip configuration."
echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
else
echo "🐍 Configuring pip..."
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
mkdir -p ~/.pip
cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple
EOF
fi
# Configure Artifactory "go" repository.
if [ -z "${REPOSITORY_GO}" ]; then
echo "🤔 no go repository is set, skipping go configuration."
echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
else
echo "🐹 Configuring go..."
jf goc --global --repo-resolve "${REPOSITORY_GO}"
fi
echo "🥳 Configuration complete!"
# Configure the JFrog CLI to use the Artifactory "docker" repository.
if [ -z "${REPOSITORY_DOCKER}" ]; then
echo "🤔 no docker repository is set, skipping docker configuration."
echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
else
if command -v docker > /dev/null 2>&1; then
echo "🔑 Configuring 🐳 docker credentials..."
mkdir -p ~/.docker
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
else
echo "🤔 no docker is installed, skipping docker configuration."
fi
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
counter=0
if [ $counter -eq 60 ]; then
echo "Timed out waiting for /tmp/code-server/bin/code-server to be installed."
exit 1
fi
echo "Waiting for /tmp/code-server/bin/code-server to be installed..."
sleep 1
((counter++))
done
echo "📦 Installing JFrog extension..."
/tmp/code-server/bin/code-server --install-extension jfrog.jfrog-vscode-extension
echo "🥳 JFrog extension installed!"
else
echo "🤔 Skipping JFrog extension installation. Set configure_code_server to true to install the JFrog extension."
fi
# Configure the JFrog CLI completion
echo "📦 Configuring JFrog CLI completion..."
# Get the user's shell
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
# Generate the completion script
jf completion $SHELLNAME --install
# Add the completion script to the user's shell profile
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
echo "" >> ~/.bashrc
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.bashrc
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
echo "# END: jf CLI shell completion" >> ~/.bashrc
else
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
fi
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
echo "" >> ~/.zshrc
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.zshrc
echo "autoload -Uz compinit" >> ~/.zshrc
echo "compinit" >> ~/.zshrc
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
echo "# END: jf CLI shell completion" >> ~/.zshrc
else
echo "🥳 ~/.zshrc already contains jf CLI shell completion configuration, skipping."
fi
else
echo "🤔 ~/.bashrc or ~/.zshrc does not exist, skipping jf CLI shell completion configuration."
fi

56
jfrog/README.md Normal file
View File

@@ -0,0 +1,56 @@
---
display_name: JFrog
description: Install the JF CLI and authenticate with Artifactory
icon: ../.icons/jfrog.svg
maintainer_github: coder
partner_github: jfrog
verified: true
tags: [integration]
---
# JFrog
Install the JF CLI and authenticate package managers with Artifactory.
```hcl
module "jfrog" {
source = "https://registry.coder.com/modules/jfrog"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
"npm": "npm-remote",
"go": "go-remote",
"pypi": "pypi-remote"
}
}
```
Get a JFrog access token from your Artifactory instance. The token must have admin permissions. It is recommended to store the token in a secret terraform variable.
```hcl
variable "artifactory_access_token" {
type = string
sensitive = true
}
```
![JFrog](../.images/jfrog.png)
## Examples
### Configure npm, go, and pypi to use Artifactory local repositories
```hcl
module "jfrog" {
source = "https://registry.coder.com/modules/jfrog"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
"npm": "npm-local",
"go": "go-local",
"pypi": "pypi-local"
}
}
```

View File

@@ -6,7 +6,7 @@ import {
testRequiredVariables, testRequiredVariables,
} from "../test"; } from "../test";
describe("jfrog-token", async () => { describe("jfrog", async () => {
await runTerraformInit(import.meta.dir); await runTerraformInit(import.meta.dir);
// Run a fake JFrog server so the provider can initialize // Run a fake JFrog server so the provider can initialize
@@ -25,7 +25,7 @@ describe("jfrog-token", async () => {
return createJSONResponse({ return createJSONResponse({
token_id: "xxx", token_id: "xxx",
access_token: "xxx", access_token: "xxx",
scopes: "any", scope: "any",
}); });
return createJSONResponse({}); return createJSONResponse({});
}, },

71
jfrog/main.tf Normal file
View File

@@ -0,0 +1,71 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
artifactory = {
source = "registry.terraform.io/jfrog/artifactory"
version = "~> 8.4.0"
}
}
}
variable "jfrog_url" {
type = string
description = "JFrog instance URL. e.g. https://YYY.jfrog.io"
}
variable "artifactory_access_token" {
type = string
description = "The admin-level access token to use for JFrog."
}
# Configure the Artifactory provider
provider "artifactory" {
url = join("/", [var.jfrog_url, "artifactory"])
access_token = var.artifactory_access_token
}
resource "artifactory_scoped_token" "me" {
# This is hacky, but on terraform plan the data source gives empty strings,
# which fails validation.
username = length(data.coder_workspace.me.owner_email) > 0 ? data.coder_workspace.me.owner_email : "plan"
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "package_managers" {
type = map(string)
description = <<EOF
A map of package manager names to their respective artifactory repositories.
For example:
{
"npm": "npm-local",
"go": "go-local",
"pypi": "pypi-local"
}
EOF
}
data "coder_workspace" "me" {}
resource "coder_script" "jfrog" {
agent_id = var.agent_id
display_name = "jfrog"
icon = "/icon/jfrog.svg"
script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url,
JFROG_HOST : replace(var.jfrog_url, "https://", ""),
ARTIFACTORY_USERNAME : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
REPOSITORY_GO : lookup(var.package_managers, "go", ""),
REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""),
})
run_on_start = true
}

49
jfrog/run.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/usr/bin/env sh
BOLD='\033[0;1m'
printf "$${BOLD}Installing JFrog CLI..."
# Install the JFrog CLI.
curl -fL https://install-cli.jfrog.io | sudo sh
sudo chmod 755 /usr/local/bin/jf
# The jf CLI checks $CI when determining whether to use interactive
# flows.
export CI=true
# Authenticate with the JFrog CLI.
jf c rm 0 || true
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" 0
# Configure the `npm` CLI to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then
echo "🤔 REPOSITORY_NPM is not set, skipping npm configuration."
else
echo "📦 Configuring npm..."
jf npmc --global --repo-resolve "${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}"
cat <<EOF >~/.npmrc
email = ${ARTIFACTORY_USERNAME}
registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
EOF
jf rt curl /api/npm/auth >>~/.npmrc
fi
# Configure the `pip` to use the Artifactory "python" repository.
if [ -z "${REPOSITORY_PYPI}" ]; then
echo "🤔 REPOSITORY_PYPI is not set, skipping pip configuration."
else
echo "🐍 Configuring pip..."
mkdir -p ~/.pip
cat <<EOF >~/.pip/pip.conf
[global]
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple
EOF
fi
# Set GOPROXY to use the Artifactory "go" repository.
if [ -z "${REPOSITORY_GO}" ]; then
echo "🤔 REPOSITORY_GO is not set, skipping go configuration."
else
echo "🐹 Configuring go..."
export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}"
fi
echo "🥳 Configuration complete!"

View File

@@ -13,10 +13,9 @@ A module that adds Jupyter Notebook in your Coder template.
![Jupyter Notebook](../.images/jupyter-notebook.png) ![Jupyter Notebook](../.images/jupyter-notebook.png)
```tf ```hcl
module "jupyter-notebook" { module "jupyter-notebook" {
source = "registry.coder.com/modules/jupyter-notebook/coder" source = "https://registry.coder.com/modules/jupyter-notebook"
version = "1.0.8"
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.17" version = ">= 0.12"
} }
} }
} }
@@ -27,21 +27,6 @@ variable "port" {
default = 19999 default = 19999
} }
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" "jupyter-notebook" { resource "coder_script" "jupyter-notebook" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "jupyter-notebook" display_name = "jupyter-notebook"
@@ -60,6 +45,5 @@ resource "coder_app" "jupyter-notebook" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "/icon/jupyter.svg" icon = "/icon/jupyter.svg"
subdomain = true subdomain = true
share = var.share share = "owner"
order = var.order
} }

View File

@@ -5,10 +5,10 @@ BOLD='\033[0;1m'
printf "$${BOLD}Installing jupyter-notebook!\n" printf "$${BOLD}Installing jupyter-notebook!\n"
# check if jupyter-notebook is installed # check if jupyter-notebook is installed
if ! command -v jupyter-notebook > /dev/null 2>&1; then if ! command -v jupyter-notebook >/dev/null 2>&1; then
# install jupyter-notebook # install jupyter-notebook
# check if python3 pip is installed # check if python3 pip is installed
if ! command -v pip3 > /dev/null 2>&1; then if ! command -v pip3 >/dev/null 2>&1; then
echo "pip3 is not installed" echo "pip3 is not installed"
echo "Please install pip3 in your Dockerfile/VM image before running this script" echo "Please install pip3 in your Dockerfile/VM image before running this script"
exit 1 exit 1
@@ -22,4 +22,4 @@ fi
echo "👷 Starting jupyter-notebook in background..." echo "👷 Starting jupyter-notebook in background..."
echo "check logs at ${LOG_PATH}" echo "check logs at ${LOG_PATH}"
$HOME/.local/bin/jupyter notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & $HOME/.local/bin/jupyter notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 &

View File

@@ -13,10 +13,9 @@ A module that adds JupyterLab in your Coder template.
![JupyterLab](../.images/jupyterlab.png) ![JupyterLab](../.images/jupyterlab.png)
```tf ```hcl
module "jupyterlab" { module "jupyterlab" {
source = "registry.coder.com/modules/jupyterlab/coder" source = "https://registry.coder.com/modules/jupyterlab"
version = "1.0.8"
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.17" version = ">= 0.12"
} }
} }
} }
@@ -27,21 +27,6 @@ variable "port" {
default = 19999 default = 19999
} }
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" "jupyterlab" { resource "coder_script" "jupyterlab" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "jupyterlab" display_name = "jupyterlab"
@@ -60,6 +45,5 @@ resource "coder_app" "jupyterlab" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "/icon/jupyter.svg" icon = "/icon/jupyter.svg"
subdomain = true subdomain = true
share = var.share share = "owner"
order = var.order
} }

View File

@@ -5,10 +5,10 @@ BOLD='\033[0;1m'
printf "$${BOLD}Installing jupyterlab!\n" printf "$${BOLD}Installing jupyterlab!\n"
# check if jupyterlab is installed # check if jupyterlab is installed
if ! command -v jupyterlab > /dev/null 2>&1; then if ! command -v jupyterlab >/dev/null 2>&1; then
# install jupyterlab # install jupyterlab
# check if python3 pip is installed # check if python3 pip is installed
if ! command -v pip3 > /dev/null 2>&1; then if ! command -v pip3 >/dev/null 2>&1; then
echo "pip3 is not installed" echo "pip3 is not installed"
echo "Please install pip3 in your Dockerfile/VM image before running this script" echo "Please install pip3 in your Dockerfile/VM image before running this script"
exit 1 exit 1
@@ -22,4 +22,4 @@ fi
echo "👷 Starting jupyterlab in background..." echo "👷 Starting jupyterlab in background..."
echo "check logs at ${LOG_PATH}" echo "check logs at ${LOG_PATH}"
$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 & $HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 &

45
lint.ts
View File

@@ -15,37 +15,7 @@ let badExit = false;
const error = (...data: any[]) => { const error = (...data: any[]) => {
console.error(...data); console.error(...data);
badExit = true; badExit = true;
}; }
const verifyCodeBlocks = (
tokens: marked.Token[],
res = {
codeIsTF: false,
codeIsHCL: false,
}
) => {
for (const token of tokens) {
// Check in-depth.
if (token.type === "list") {
verifyCodeBlocks(token.items, res);
continue;
}
if (token.type === "list_item") {
verifyCodeBlocks(token.tokens, res);
continue;
}
if (token.type === "code") {
if (token.lang === "tf") {
res.codeIsTF = true;
}
if (token.lang === "hcl") {
res.codeIsHCL = true;
}
}
}
return res;
};
// Ensures that each README has the proper format. // Ensures that each README has the proper format.
// Exits with 0 if all is good! // Exits with 0 if all is good!
@@ -92,7 +62,6 @@ for (const dir of dirs) {
let h1 = false; let h1 = false;
let code = false; let code = false;
let paragraph = false; let paragraph = false;
let version = true;
for (const token of tokens) { for (const token of tokens) {
if (token.type === "heading" && token.depth === 1) { if (token.type === "heading" && token.depth === 1) {
@@ -108,10 +77,6 @@ for (const dir of dirs) {
} }
if (token.type === "code") { if (token.type === "code") {
code = true; code = true;
if (token.lang === "tf" && !token.text.includes("version")) {
version = false;
error(dir.name, "missing version in tf code block");
}
continue; continue;
} }
} }
@@ -124,14 +89,6 @@ for (const dir of dirs) {
if (!code) { if (!code) {
error(dir.name, "missing example code block after paragraph"); error(dir.name, "missing example code block after paragraph");
} }
const { codeIsTF, codeIsHCL } = verifyCodeBlocks(tokens);
if (!codeIsTF) {
error(dir.name, "missing example tf code block");
}
if (codeIsHCL) {
error(dir.name, "hcl code block should be tf");
}
} }
if (badExit) { if (badExit) {

4
new.sh
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This scripts creates a new sample moduledir with required files # This scripts creates a new sample moduledir with requried files
# Run it like : ./new.sh my-module # Run it like : ./new.sh my-module
MODULE_NAME=$1 MODULE_NAME=$1
@@ -11,7 +11,7 @@ if [ -z "$MODULE_NAME" ]; then
exit 1 exit 1
fi fi
# Create module directory and exit if it already exists # Create module directory and exit if it alredy exists
if [ -d "$MODULE_NAME" ]; then if [ -d "$MODULE_NAME" ]; then
echo "Module with name $MODULE_NAME already exists" echo "Module with name $MODULE_NAME already exists"
echo "Please choose a different name" echo "Please choose a different name"

View File

@@ -1,58 +0,0 @@
---
display_name: nodejs
description: Install Node.js via nvm
icon: ../.icons/node.svg
maintainer_github: TheZoker
verified: false
tags: [helper]
---
# nodejs
Automatically installs [Node.js](https://github.com/nodejs/node) via [nvm](https://github.com/nvm-sh/nvm). It can also install multiple versions of node and set a default version. If no options are specified, the latest version is installed.
```tf
module "nodejs" {
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.8"
agent_id = coder_agent.example.id
}
```
### Install multiple versions
This installs multiple versions of Node.js:
```tf
module "nodejs" {
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.8"
agent_id = coder_agent.example.id
node_versions = [
"18",
"20",
"node"
]
default_node_version = "20"
}
```
### Full example
A example with all available options:
```tf
module "nodejs" {
source = "registry.coder.com/modules/nodejs/coder"
version = "1.0.8"
agent_id = coder_agent.example.id
nvm_version = "v0.39.7"
nvm_install_prefix = "/opt/nvm"
node_versions = [
"16",
"18",
"node"
]
default_node_version = "16"
}
```

View File

@@ -1,12 +0,0 @@
import { describe, expect, it } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
describe("nodejs", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
// More tests depend on shebang refactors
});

View File

@@ -1,52 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "nvm_version" {
type = string
description = "The version of nvm to install."
default = "master"
}
variable "nvm_install_prefix" {
type = string
description = "The prefix to install nvm to."
default = "$HOME/.nvm"
}
variable "node_versions" {
type = list(string)
description = "A list of Node.js versions to install."
default = ["node"]
}
variable "default_node_version" {
type = string
description = "The default Node.js version"
default = "node"
}
resource "coder_script" "nodejs" {
agent_id = var.agent_id
display_name = "Node.js:"
script = templatefile("${path.module}/run.sh", {
NVM_VERSION : var.nvm_version,
INSTALL_PREFIX : var.nvm_install_prefix,
NODE_VERSIONS : join(",", var.node_versions),
DEFAULT : var.default_node_version,
})
run_on_start = true
start_blocks_login = true
}

View File

@@ -1,50 +0,0 @@
#!/usr/bin/env bash
NVM_VERSION='${NVM_VERSION}'
NODE_VERSIONS='${NODE_VERSIONS}'
INSTALL_PREFIX='${INSTALL_PREFIX}'
DEFAULT='${DEFAULT}'
BOLD='\033[0;1m'
CODE='\033[36;40;1m'
RESET='\033[0m'
printf "$${BOLD}Installing nvm!$${RESET}\n"
export NVM_DIR="$${INSTALL_PREFIX}/nvm"
script="$(curl -sS -o- "https://raw.githubusercontent.com/nvm-sh/nvm/$${NVM_VERSION}/install.sh" 2>&1)"
if [ $? -ne 0 ]; then
echo "Failed to download nvm installation script: $script"
exit 1
fi
output="$(bash <<< "$script" 2>&1)"
if [ $? -ne 0 ]; then
echo "Failed to install nvm: $output"
exit 1
fi
printf "🥳 nvm has been installed\n\n"
# Set up nvm for the rest of the script.
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
# Install each node version...
IFS=',' read -r -a VERSIONLIST <<< "$${NODE_VERSIONS}"
for version in "$${VERSIONLIST[@]}"; do
if [ -z "$version" ]; then
continue
fi
printf "🛠️ Installing node version $${CODE}$version$${RESET}...\n"
output=$(nvm install "$version" 2>&1)
if [ $? -ne 0 ]; then
echo "Failed to install version: $version: $output"
exit 1
fi
done
# Set default if provided
if [ -n "$${DEFAULT}" ]; then
printf "🛠️ Setting default node version $${CODE}$DEFAULT$${RESET}...\n"
output=$(nvm alias default $DEFAULT 2>&1)
fi

View File

@@ -2,25 +2,16 @@
"name": "modules", "name": "modules",
"scripts": { "scripts": {
"test": "bun test", "test": "bun test",
"fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", "fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.tf",
"fmt:ci": "bun x prettier --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.tf .sample/main.tf", "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf",
"lint": "bun run lint.ts && ./terraform_validate.sh", "lint": "bun run lint.ts"
"update-version": "./update-version.sh"
}, },
"devDependencies": { "devDependencies": {
"bun-types": "^1.0.18", "bun-types": "^1.0.3",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^12.0.0", "marked": "^9.0.3"
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.3.3" "typescript": "^5.0.0"
},
"prettier": {
"plugins": [
"prettier-plugin-sh",
"prettier-plugin-terraform-formatter"
]
} }
} }

View File

@@ -11,10 +11,9 @@ tags: [helper]
Run a script on workspace start that allows developers to run custom commands to personalize their workspace. Run a script on workspace start that allows developers to run custom commands to personalize their workspace.
```tf ```hcl
module "personalize" { module "personalize" {
source = "registry.coder.com/modules/personalize/coder" source = "https://registry.coder.com/modules/personalize"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -12,7 +12,7 @@ tags: [helper]
Add the `slackme` command to your workspace that DMs you on Slack when your command finishes running. Add the `slackme` command to your workspace that DMs you on Slack when your command finishes running.
```bash ```bash
slackme npm run long-build $ slackme npm run long-build
``` ```
## Setup ## Setup
@@ -54,10 +54,9 @@ slackme npm run long-build
3. Restart your Coder deployment. Any Template can now import the Slack Me module, and `slackme` will be available on the `$PATH`: 3. Restart your Coder deployment. Any Template can now import the Slack Me module, and `slackme` will be available on the `$PATH`:
```tf ```hcl
module "slackme" { module "slackme" {
source = "registry.coder.com/modules/slackme/coder" source = "https://registry.coder.com/modules/slackme"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
} }
@@ -70,10 +69,9 @@ slackme npm run long-build
- `$COMMAND` is replaced with the command the user executed. - `$COMMAND` is replaced with the command the user executed.
- `$DURATION` is replaced with a human-readable duration the command took to execute. - `$DURATION` is replaced with a human-readable duration the command took to execute.
```tf ```hcl
module "slackme" { module "slackme" {
source = "registry.coder.com/modules/slackme/coder" source = "https://registry.coder.com/modules/slackme"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
slack_message = <<EOF slack_message = <<EOF

View File

@@ -56,6 +56,15 @@ describe("slackme", async () => {
}); });
}); });
it("exits with command code", async () => {
const { instance, id } = await setupContainer();
await writeCoder(id, "echo 'some-url' && exit 1");
let exec = await execContainer(id, ["sh", "-c", instance.script]);
expect(exec.exitCode).toBe(0);
exec = await execContainer(id, ["sh", "-c", "slackme exit 1"]);
expect(exec.exitCode).toBe(1);
});
it("formats multiline message", async () => { it("formats multiline message", async () => {
await assertSlackMessage({ await assertSlackMessage({
command: "echo test", command: "echo test",

View File

@@ -1,15 +1,14 @@
#!/usr/bin/env sh #!/usr/bin/env sh
PROVIDER_ID=${PROVIDER_ID} PROVIDER_ID=${PROVIDER_ID}
SLACK_MESSAGE=$( SLACK_MESSAGE=$(cat << "EOF"
cat << "EOF"
${SLACK_MESSAGE} ${SLACK_MESSAGE}
EOF EOF
) )
SLACK_URL=$${SLACK_URL:-https://slack.com} SLACK_URL=$${SLACK_URL:-https://slack.com}
usage() { usage() {
cat << EOF cat <<EOF
slackme — Send a Slack notification when a command finishes slackme — Send a Slack notification when a command finishes
Usage: slackme <command> Usage: slackme <command>
@@ -73,9 +72,10 @@ fi
START=$(date +%s%N) START=$(date +%s%N)
# Run all arguments as a command # Run all arguments as a command
$@ "$@"
CODE=$?
END=$(date +%s%N) END=$(date +%s%N)
DURATION_MS=$${DURATION_MS:-$(((END - START) / 1000000))} DURATION_MS=$${DURATION_MS:-$(( (END - START) / 1000000 ))}
PRETTY_DURATION=$(pretty_duration $DURATION_MS) PRETTY_DURATION=$(pretty_duration $DURATION_MS)
set -e set -e
@@ -86,3 +86,5 @@ SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g")
curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \ curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \
-G --data-urlencode "text=$${SLACK_MESSAGE}" \ -G --data-urlencode "text=$${SLACK_MESSAGE}" \
"$SLACK_URL/api/chat.postMessage?channel=$USER_ID&pretty=1" "$SLACK_URL/api/chat.postMessage?channel=$USER_ID&pretty=1"
exit $CODE

View File

@@ -1,29 +0,0 @@
#!/bin/bash
set -euo pipefail
# Function to run terraform init and validate in a directory
run_terraform() {
local dir="$1"
echo "Running terraform init and validate in $dir"
pushd "$dir"
terraform init -upgrade
terraform validate
popd
}
# Main script
main() {
# Get the directory of the script
script_dir=$(dirname "$(readlink -f "$0")")
# Get all subdirectories in the repository
subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort)
for dir in $subdirs; do
run_terraform "$dir"
done
}
# Run the main script
main

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# This script updates the version number in the README.md files of all modules
# to the latest tag in the repository. It is intended to be run from the root
# of the repository or by using the `bun update-version` command.
set -euo pipefail
current_tag=$(git describe --tags --abbrev=0)
previous_tag=$(git describe --tags --abbrev=0 $current_tag^)
mapfile -t changed_dirs < <(git diff --name-only "$previous_tag"..."$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u)
LATEST_TAG=$(git describe --abbrev=0 --tags | sed 's/^v//') || exit $?
for dir in "${changed_dirs[@]}"; do
if [[ -f "$dir/README.md" ]]; then
echo "Bumping version in $dir/README.md"
file="$dir/README.md"
tmpfile=$(mktemp /tmp/tempfile.XXXXXX)
awk -v tag="$LATEST_TAG" '{
if ($1 == "version" && $2 == "=") {
sub(/"[^"]*"/, "\"" tag "\"")
print
} else {
print
}
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
fi
done

View File

@@ -1,79 +0,0 @@
---
display_name: Hashicorp Vault Integration (GitHub)
description: Authenticates with Vault using GitHub
icon: ../.icons/vault.svg
maintainer_github: coder
partner_github: hashicorp
verified: true
tags: [helper, integration, vault, github]
---
# Hashicorp Vault Integration (GitHub)
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using [external auth](https://coder.com/docs/v2/latest/admin/external-auth) for GitHub.
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
```
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
```shell
vault kv get -namespace=coder -mount=secrets coder
```
or using the Vault API:
```shell
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
```
![Vault login](../.images/vault-login.png)
## Configuration
To configure the Vault module, you must set up a Vault GitHub auth method. See the [Vault documentation](https://www.vaultproject.io/docs/auth/github) for more information.
## Examples
### Configure Vault integration with a different Coder GitHub external auth ID (i.e., not the default `github`)
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
}
```
### Configure Vault integration with a different Coder GitHub external auth ID and a different Vault GitHub auth path
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
vault_github_auth_path = "my-github-auth-path"
}
```
### Configure Vault integration and install a specific version of the Vault CLI
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0"
}
```

View File

@@ -1,11 +0,0 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
describe("vault-github", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
vault_addr: "foo",
});
});

View File

@@ -1,68 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "vault_addr" {
type = string
description = "The address of the Vault server."
}
variable "coder_github_auth_id" {
type = string
description = "The ID of the GitHub external auth."
default = "github"
}
variable "vault_github_auth_path" {
type = string
description = "The path to the GitHub auth method."
default = "github"
}
variable "vault_cli_version" {
type = string
description = "The version of Vault to install."
default = "latest"
validation {
condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.vault_cli_version))
error_message = "Vault version must be in the format 0.0.0 or latest"
}
}
data "coder_workspace" "me" {}
resource "coder_script" "vault" {
agent_id = var.agent_id
display_name = "Vault (GitHub)"
icon = "/icon/vault.svg"
script = templatefile("${path.module}/run.sh", {
AUTH_PATH : var.vault_github_auth_path,
GITHUB_EXTERNAL_AUTH_ID : data.coder_external_auth.github.id,
INSTALL_VERSION : var.vault_cli_version,
})
run_on_start = true
start_blocks_login = true
}
resource "coder_env" "vault_addr" {
agent_id = var.agent_id
name = "VAULT_ADDR"
value = var.vault_addr
}
data "coder_external_auth" "github" {
id = var.coder_github_auth_id
}

View File

@@ -1,119 +0,0 @@
#!/usr/bin/env bash
# Convert all templated variables to shell variables
INSTALL_VERSION=${INSTALL_VERSION}
GITHUB_EXTERNAL_AUTH_ID=${GITHUB_EXTERNAL_AUTH_ID}
AUTH_PATH=${AUTH_PATH}
fetch() {
dest="$1"
url="$2"
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$${url}" -o "$${dest}"
elif command -v wget > /dev/null 2>&1; then
wget -O "$${dest}" "$${url}"
elif command -v busybox > /dev/null 2>&1; then
busybox wget -O "$${dest}" "$${url}"
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
exit 1
fi
}
unzip_safe() {
if command -v unzip > /dev/null 2>&1; then
command unzip "$@"
elif command -v busybox > /dev/null 2>&1; then
busybox unzip "$@"
else
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
exit 1
fi
}
install() {
# Get the architecture of the system
ARCH=$(uname -m)
if [ "$${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "$${ARCH}" = "aarch64" ]; then
ARCH="arm64"
else
printf "Unsupported architecture: $${ARCH}\n"
return 1
fi
# Fetch the latest version of Vault if INSTALL_VERSION is 'latest'
if [ "$${INSTALL_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n"
return 1
fi
INSTALL_VERSION=$${LATEST_VERSION}
fi
# Check if the vault CLI is installed and has the correct version
installation_needed=1
if command -v vault > /dev/null 2>&1; then
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
if [ "$${CURRENT_VERSION}" = "$${INSTALL_VERSION}" ]; then
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
installation_needed=0
fi
fi
if [ $${installation_needed} -eq 1 ]; then
# Download and install Vault
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}"
fi
fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip"
if [ $? -ne 0 ]; then
printf "Failed to download Vault.\n"
return 1
fi
if ! unzip_safe vault.zip; then
printf "Failed to unzip Vault.\n"
return 1
fi
rm vault.zip
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
printf "Vault installed successfully!\n\n"
else
mkdir -p ~/.local/bin
if ! mv vault ~/.local/bin/vault; then
printf "Failed to move Vault to local bin.\n"
return 1
fi
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
fi
fi
return 0
}
TMP=$(mktemp -d)
if ! (
cd "$TMP"
install
); then
echo "Failed to install Vault CLI."
exit 1
fi
rm -rf "$TMP"
# Authenticate with Vault
printf "🔑 Authenticating with Vault ...\n\n"
GITHUB_TOKEN=$(coder external-auth access-token "$${GITHUB_EXTERNAL_AUTH_ID}")
if [ $? -ne 0 ]; then
printf "Authentication with Vault failed. Please check your credentials.\n"
exit 1
fi
# Login to vault using the GitHub token
printf "🔑 Logging in to Vault ...\n\n"
vault login -no-print -method=github -path=/$${AUTH_PATH} token="$${GITHUB_TOKEN}"
printf "🥳 Vault authentication complete!\n\n"
printf "You can now use Vault CLI to access secrets.\n"

View File

@@ -1,83 +0,0 @@
---
display_name: Hashicorp Vault Integration (Token)
description: Authenticates with Vault using Token
icon: ../.icons/vault.svg
maintainer_github: coder
partner_github: hashicorp
verified: true
tags: [helper, integration, vault, token]
---
# Hashicorp Vault Integration (Token)
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using a [Vault token](https://developer.hashicorp.com/vault/docs/auth/token).
```tf
variable "vault_token" {
type = string
description = "The Vault token to use for authentication."
sensitive = true
}
module "vault" {
source = "registry.coder.com/modules/vault-token/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_token = var.token
vault_addr = "https://vault.example.com"
}
```
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
```shell
vault kv get -namespace=coder -mount=secrets coder
```
or using the Vault API:
```shell
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
```
## Configuration
To configure the Vault module, you must create a Vault token with the the required permissions and configure the module with the token and Vault address.
1. Create a vault policy with read access to the secret mount you need your developers to access.
```shell
vault policy write read-coder-secrets - <<EOF
path "coder/data/*" {
capabilities = ["read"]
}
path "coder/metadata/*" {
capabilities = ["read"]
}
EOF
```
2. Create a token using this policy.
```shell
vault token create -policy="read-coder-secrets"
```
3. Copy the generated token and use in your template.
## Examples
### Configure Vault integration and install a specific version of the Vault CLI
```tf
variable "vault_token" {
type = string
description = "The Vault token to use for authentication."
sensitive = true
}
module "vault" {
source = "registry.coder.com/modules/vault-token/coder"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token
vault_cli_version = "1.15.0"
}
```

View File

@@ -1,12 +0,0 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
describe("vault-token", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
vault_addr: "foo",
vault_token: "foo",
});
});

View File

@@ -1,62 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "vault_addr" {
type = string
description = "The address of the Vault server."
}
variable "vault_token" {
type = string
description = "The Vault token to use for authentication."
sensitive = true
}
variable "vault_cli_version" {
type = string
description = "The version of Vault to install."
default = "latest"
validation {
condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.vault_cli_version))
error_message = "Vault version must be in the format 0.0.0 or latest"
}
}
data "coder_workspace" "me" {}
resource "coder_script" "vault" {
agent_id = var.agent_id
display_name = "Vault (Token)"
icon = "/icon/vault.svg"
script = templatefile("${path.module}/run.sh", {
INSTALL_VERSION : var.vault_cli_version,
})
run_on_start = true
start_blocks_login = true
}
resource "coder_env" "vault_addr" {
agent_id = var.agent_id
name = "VAULT_ADDR"
value = var.vault_addr
}
resource "coder_env" "vault_token" {
agent_id = var.agent_id
name = "VAULT_TOKEN"
value = var.vault_token
}

View File

@@ -1,103 +0,0 @@
#!/usr/bin/env bash
# Convert all templated variables to shell variables
INSTALL_VERSION=${INSTALL_VERSION}
fetch() {
dest="$1"
url="$2"
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$${url}" -o "$${dest}"
elif command -v wget > /dev/null 2>&1; then
wget -O "$${dest}" "$${url}"
elif command -v busybox > /dev/null 2>&1; then
busybox wget -O "$${dest}" "$${url}"
else
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
return 1
fi
}
unzip_safe() {
if command -v unzip > /dev/null 2>&1; then
command unzip "$@"
elif command -v busybox > /dev/null 2>&1; then
busybox unzip "$@"
else
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
return 1
fi
}
install() {
# Get the architecture of the system
ARCH=$(uname -m)
if [ "$${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "$${ARCH}" = "aarch64" ]; then
ARCH="arm64"
else
printf "Unsupported architecture: $${ARCH}\n"
return 1
fi
# Fetch the latest version of Vault if INSTALL_VERSION is 'latest'
if [ "$${INSTALL_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n"
return 1
fi
INSTALL_VERSION=$${LATEST_VERSION}
fi
# Check if the vault CLI is installed and has the correct version
installation_needed=1
if command -v vault > /dev/null 2>&1; then
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
if [ "$${CURRENT_VERSION}" = "$${INSTALL_VERSION}" ]; then
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
installation_needed=0
fi
fi
if [ $${installation_needed} -eq 1 ]; then
# Download and install Vault
if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n"
else
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}"
fi
fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_amd64.zip"
if [ $? -ne 0 ]; then
printf "Failed to download Vault.\n"
return 1
fi
if ! unzip_safe vault.zip; then
printf "Failed to unzip Vault.\n"
return 1
fi
rm vault.zip
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
printf "Vault installed successfully!\n\n"
else
mkdir -p ~/.local/bin
if ! mv vault ~/.local/bin/vault; then
printf "Failed to move Vault to local bin.\n"
return 1
fi
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
fi
fi
return 0
}
TMP=$(mktemp -d)
if ! (
cd "$TMP"
install
); then
echo "Failed to install Vault CLI."
exit 1
fi
rm -rf "$TMP"

View File

@@ -13,10 +13,9 @@ Add a button to open any workspace with a single click.
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
```tf ```hcl
module "vscode" { module "vscode" {
source = "registry.coder.com/modules/vscode-desktop/coder" source = "https://registry.coder.com/modules/vscode-desktop"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -25,10 +24,9 @@ module "vscode" {
### Open in a specific directory ### Open in a specific directory
```tf ```hcl
module "vscode" { module "vscode" {
source = "registry.coder.com/modules/vscode-desktop/coder" source = "https://registry.coder.com/modules/vscode-desktop"
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

@@ -20,18 +20,5 @@ describe("vscode-desktop", async () => {
expect(state.outputs.vscode_url.value).toBe( expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN", "vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
); );
const resources: any = state.resources;
expect(resources[1].instances[0].attributes.order).toBeNull();
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const resources: any = state.resources;
expect(resources[1].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.17" version = ">= 0.12"
} }
} }
} }
@@ -20,12 +20,6 @@ variable "folder" {
default = "" default = ""
} }
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
resource "coder_app" "vscode" { resource "coder_app" "vscode" {
@@ -34,7 +28,6 @@ resource "coder_app" "vscode" {
icon = "/icon/code.svg" icon = "/icon/code.svg"
slug = "vscode" slug = "vscode"
display_name = "VS Code Desktop" display_name = "VS Code Desktop"
order = var.order
url = var.folder != "" ? join("", [ url = var.folder != "" ? join("", [
"vscode://coder.coder-remote/open?owner=", "vscode://coder.coder-remote/open?owner=",
data.coder_workspace.me.owner, data.coder_workspace.me.owner,
@@ -42,8 +35,6 @@ resource "coder_app" "vscode" {
data.coder_workspace.me.name, data.coder_workspace.me.name,
"&folder=", "&folder=",
var.folder, var.folder,
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN", "&token=$SESSION_TOKEN",
]) : join("", [ ]) : join("", [
"vscode://coder.coder-remote/open?owner=", "vscode://coder.coder-remote/open?owner=",

View File

@@ -9,12 +9,11 @@ tags: [helper, ide, vscode, web]
# VS Code Web # VS Code Web
Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace and create an app to access it via the dashboard. Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace using the [VS Code CLI](https://code.visualstudio.com/docs/editor/command-line) and create an app to access it via the dashboard.
```tf ```hcl
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "https://registry.coder.com/modules/vscode-web"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
accept_license = true accept_license = true
} }
@@ -26,25 +25,12 @@ module "vscode-web" {
### Install VS Code Web to a custom folder ### Install VS Code Web to a custom folder
```tf ```hcl
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "https://registry.coder.com/modules/vscode-web"
version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web" install_dir = "/home/coder/.vscode-web"
folder = "/home/coder" folder = "/home/coder"
accept_license = true accept_license = true
} }
``` ```
### Install Extensions
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.8"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
}
```

63
vscode-web/main.test.ts Normal file
View File

@@ -0,0 +1,63 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
} from "../test";
describe("vscode-web", async () => {
await runTerraformInit(import.meta.dir);
// replaces testRequiredVariables due to license variable
// may add a testRequiredVariablesWithLicense function later
it("missing agent_id", async () => {
try {
await runTerraformApply(import.meta.dir, {
accept_license: "true",
});
} catch (ex) {
expect(ex.message).toContain('input variable "agent_id" is not set');
}
});
it("invalid license_agreement", async () => {
try {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
} catch (ex) {
expect(ex.message).toContain(
"You must accept the VS Code license agreement by setting accept_license=true",
);
}
});
it("fails without curl", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
});
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(1);
expect(output.stdout).toEqual([
"\u001b[0;1mInstalling vscode-cli!",
"Failed to install vscode-cli:", // TODO: manually test error log
]);
});
it("runs with curl", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
accept_license: "true",
});
const output = await executeScriptInContainer(state, "alpine/curl");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"\u001b[0;1mInstalling vscode-cli!",
"🥳 vscode-cli has been installed.",
"",
"👷 Running /tmp/vscode-cli/bin/code serve-web --port 13338 --without-connection-token --accept-server-license-terms in the background...",
"Check logs at /tmp/vscode-web.log!",
]);
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.17" version = ">= 0.12"
} }
} }
} }
@@ -20,54 +20,27 @@ variable "port" {
default = 13338 default = 13338
} }
variable "display_name" {
type = string
description = "The display name for the VS Code Web application."
default = "VS Code Web"
}
variable "slug" {
type = string
description = "The slug for the VS Code Web application."
default = "vscode-web"
}
variable "folder" { variable "folder" {
type = string type = string
description = "The folder to open in vscode-web." description = "The folder to open in vscode-web."
default = "" default = ""
} }
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 "log_path" { variable "log_path" {
type = string type = string
description = "The path to log." description = "The path to log."
default = "/tmp/vscode-web.log" default = "/tmp/vscode-web.log"
} }
variable "install_prefix" { variable "install_dir" {
type = string type = string
description = "The prefix to install vscode-web to." description = "The directory to install VS Code CLI"
default = "/tmp/vscode-web" default = "/tmp/vscode-cli"
}
variable "extensions" {
type = list(string)
description = "A list of extensions to install."
default = []
} }
variable "accept_license" { variable "accept_license" {
type = bool type = bool
description = "Accept the VS Code Server license. https://code.visualstudio.com/license/server" description = "Accept the VS Code license. https://code.visualstudio.com/license"
default = false default = false
validation { validation {
condition = var.accept_license == true condition = var.accept_license == true
@@ -75,22 +48,6 @@ variable "accept_license" {
} }
} }
variable "telemetry_level" {
type = string
description = "Set the telemetry level for VS Code Web."
default = "error"
validation {
condition = var.telemetry_level == "off" || var.telemetry_level == "crash" || var.telemetry_level == "error" || var.telemetry_level == "all"
error_message = "Incorrect value. Please set either 'off', 'crash', 'error', or 'all'."
}
}
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" "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"
@@ -98,22 +55,19 @@ resource "coder_script" "vscode-web" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
PORT : var.port, PORT : var.port,
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
INSTALL_PREFIX : var.install_prefix, INSTALL_DIR : var.install_dir,
EXTENSIONS : join(",", var.extensions),
TELEMETRY_LEVEL : var.telemetry_level,
}) })
run_on_start = true run_on_start = true
} }
resource "coder_app" "vscode-web" { resource "coder_app" "vscode-web" {
agent_id = var.agent_id agent_id = var.agent_id
slug = var.slug slug = "vscode-web"
display_name = var.display_name display_name = "VS Code Web"
url = var.folder == "" ? "http://localhost:${var.port}" : "http://localhost:${var.port}?folder=${var.folder}" url = var.folder == "" ? "http://localhost:${var.port}" : "http://localhost:${var.port}?folder=${var.folder}"
icon = "/icon/code.svg" icon = "/icon/code.svg"
subdomain = true subdomain = true
share = var.share share = "owner"
order = var.order
healthcheck { healthcheck {
url = "http://localhost:${var.port}/healthz" url = "http://localhost:${var.port}/healthz"

48
vscode-web/run.sh Executable file → Normal file
View File

@@ -1,49 +1,21 @@
#!/usr/bin/env bash #!/usr/bin/env sh
BOLD='\033[0;1m' BOLD='\033[0;1m'
EXTENSIONS=("${EXTENSIONS}")
# Create install prefix # Create install directory if it doesn't exist
mkdir -p ${INSTALL_PREFIX} mkdir -p ${INSTALL_DIR}
printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n" printf "$${BOLD}Installing vscode-cli!\n"
# Download and extract vscode-server # Download and extract code-cli tarball
ARCH=$(uname -m) output=$(curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64' --output vscode_cli.tar.gz && tar -xf vscode_cli.tar.gz -C ${INSTALL_DIR} && rm vscode_cli.tar.gz)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
*)
echo "Unsupported architecture"
exit 1
;;
esac
HASH=$(curl https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2)
output=$(curl -sL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to install Microsoft Visual Studio Code Server: $output" echo "Failed to install vscode-cli: $output"
exit 1 exit 1
fi fi
printf "$${BOLD}Microsoft Visual Studio Code Server has been installed.\n" printf "🥳 vscode-cli has been installed.\n\n"
VSCODE_SERVER="${INSTALL_PREFIX}/bin/code-server" echo "👷 Running ${INSTALL_DIR}/bin/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms in the background..."
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($VSCODE_SERVER --install-extension "$extension" --force)
if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output"
exit 1
fi
done
echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!" echo "Check logs at ${LOG_PATH}!"
"${INSTALL_PREFIX}/bin/code-server" serve-local --port "${PORT}" --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 & ${INSTALL_DIR}/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms >${LOG_PATH} 2>&1 &