Compare commits

..

85 Commits

Author SHA1 Message Date
Ben Potter
cb075aa035 fix order 2024-06-26 16:44:21 -05:00
Ben Potter
9864408643 fix: aws starter template compatability 2024-06-26 16:34:52 -05:00
Parkreiner
de00f6334f chore: add type parameter for testRequiredVariables 2024-06-26 19:00:42 +00:00
Parkreiner
264584e673 fix: make comments for test helpers exportable 2024-06-26 17:59:12 +00:00
Parkreiner
83ecba2293 wip: commit current progress 2024-06-26 17:21:39 +00:00
Parkreiner
b2807640aa wip: commit progress on main test file 2024-06-26 16:01:08 +00:00
Parkreiner
33d44fdf17 fix: remove unneeded any types 2024-06-26 16:00:57 +00:00
Parkreiner
f335cd343d fix: update type definitions for helpers 2024-06-26 16:00:40 +00:00
Parkreiner
aebf095075 refactor: clean up patch logic for clarity 2024-06-26 14:37:14 +00:00
Parkreiner
b283ac3129 docs: fix misleading typo in comment 2024-06-25 21:54:13 +00:00
Parkreiner
5f418c3253 docs: add comments about necessary double dollar signs 2024-06-25 21:51:21 +00:00
Parkreiner
b09c4cb084 fix: speed up code for filling in form 2024-06-25 21:35:53 +00:00
Parkreiner
8aff87fdf7 fix: add logic for hiding the dropdown of protocol options 2024-06-25 21:20:42 +00:00
Parkreiner
f3c30abeb4 fix: make form hiding logic run on webpage opening 2024-06-25 21:03:02 +00:00
Parkreiner
a9a75b675f fix: add more changes to opacity logic 2024-06-25 21:01:11 +00:00
Parkreiner
ef4c87e48e fix: simplify code for hiding form 2024-06-25 20:45:39 +00:00
Parkreiner
1a0a8659cc wip: update logic for hiding form to avoid whiffs 2024-06-25 20:40:44 +00:00
Parkreiner
c7a4fced4c fix: update instanceof check 2024-06-25 20:15:18 +00:00
Parkreiner
5ec1b207d1 docs: remove now-inaccurate comment 2024-06-25 19:58:56 +00:00
Parkreiner
702271133f fix: update HTML query selector 2024-06-25 19:57:48 +00:00
Parkreiner
652fc6b84f refactor: clean up form code 2024-06-25 19:55:14 +00:00
Parkreiner
8195cf4453 wip: add current code for hiding Devolutions form 2024-06-25 19:48:44 +00:00
Parkreiner
d5cfadb4e7 fix: remove template literal dollar signs 2024-06-25 17:03:54 +00:00
Parkreiner
fba0f842a9 fix: remove regex search from Select-String 2024-06-24 21:47:01 +00:00
Parkreiner
14e3fc5b6b fix: whitespace 2024-06-24 21:13:15 +00:00
Parkreiner
0b6975c266 fix: escape quotes 2024-06-24 20:41:45 +00:00
Parkreiner
d530d68b12 fix: more money, more problems 2024-06-24 20:28:44 +00:00
Parkreiner
047ccd67ca fix: dolla dolla 2024-06-24 20:24:49 +00:00
Parkreiner
c7aa8253e3 fix: dolla dolla 2024-06-24 20:22:25 +00:00
Parkreiner
452f41aa86 fix: add parenthesis 2024-06-24 20:17:31 +00:00
Parkreiner
29209d546e fix: update typo in powershell script
Co-authored-by: Asher <ash@coder.com>
2024-06-24 20:13:11 +00:00
Parkreiner
aab5e55663 fix: update script frequency 2024-06-24 20:10:22 +00:00
Parkreiner
ff96b3f653 wip: commit current progress for devolutions patch 2024-06-24 20:07:39 +00:00
Parkreiner
20795aa2b6 chore: add script file for overriding Devolutions 2024-06-24 19:37:31 +00:00
Ben
b93471a381 chore: add admin username 2024-04-24 22:39:24 +00:00
Ben
53083a5718 add more context on auto login 2024-04-06 20:46:50 +00:00
Ben
7de78d2ef5 add tags 2024-04-06 20:36:55 +00:00
Ben
89135671b2 fix module usage 2024-04-06 20:34:06 +00:00
Ben
ac648cc0a9 add thumbnail 2024-04-06 20:32:52 +00:00
Ben
748a180ac3 add temp link to example template 2024-04-06 20:18:58 +00:00
Ben
ec922c7c3d remove metadata for now 2024-04-06 20:13:50 +00:00
Ben
9f8eee55b2 rename script 2024-04-06 20:11:59 +00:00
Ben
0e7644b284 remove count 2024-04-06 20:05:57 +00:00
Ben
bf06e8d3ac fix agent id 2024-04-06 20:04:28 +00:00
Ben
12fd16f701 add metadata and local instructions 2024-04-06 20:01:57 +00:00
Ben
1197e6bf0d fix port typo 2024-04-06 19:51:51 +00:00
Ben
c5c521fabd feat: add web RDP module 2024-04-06 19:48:37 +00:00
Muhammad Atif Ali
838ec95875 fix(vscode-web): use --host 127.0.01 (#216)
MS code-server defaults to using `--host localhost`, which was working perfectly fine with Coder.

But recently Coder is failing to proxy vscode-web with the https://github.com/coder/coder/issues/12790 

As a workaround setting `--host 127.0.0.1` works.
2024-04-05 20:05:30 +03:00
Stephen Kirby
5a0efdf867 Merge pull request #213 from coder/new-jfrog-logo
update jfrog logo
2024-04-04 14:16:13 -05:00
Stephen Kirby
4debc3200d update jfrog logo 2024-04-03 17:29:30 +00:00
Michael Brewer
5476f819ce chore(git-commit-signing): use included icon for git (#203) 2024-03-30 14:15:12 +03:00
Michael Brewer
9a5ff6df64 feat(jetbrain-gateway): add coder_pameter order (#208) 2024-03-30 14:14:43 +03:00
github-actions[bot]
bab0f7d24d chore: bump version to 1.0.10 in README.md files (#202)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-03-20 01:07:23 +03:00
Michael Brewer
fc914626a2 feat(code-server): add code-server offline and cache support (#184) 2024-03-19 13:50:32 -08:00
github-actions[bot]
fdbb2e30d0 chore: bump version to 1.0.10 in README.md files (#201)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-03-19 23:11:46 +03:00
Florian Gareis
ee80d1f64c Fix nodejs: Create directory before executing script (#183) 2024-03-19 23:09:47 +03:00
Muhammad Atif Ali
017f007bde chore(jetbrains-gateway): match example with screenshot (#200) 2024-03-18 22:39:23 +03:00
Michael Brewer
18810cc51e feat(vscode-web): add support for settings (#195)
Currently saves to `~/.vscode-server/data/Machine/settings.json`
as `~/.vscode-server/data/User/settings.json` will not work because
VS Code web stores user settings in the browser.
2024-03-18 10:46:51 -08:00
Muhammad Atif Ali
98a428ae89 chore(jfrog-token): set token description (#198) 2024-03-18 13:35:18 +03:00
Michael Brewer
dd072e261a feat(aws-region): add missing regions (#197) 2024-03-15 23:51:12 +03:00
github-actions[bot]
7e3743739e chore: bump version to 1.0.9 in README.md files (#193)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-03-15 04:44:43 +03:00
Michael Brewer
f5681b5206 feat(jetbrains-gateway): update ide versions to 2023.3.* (#191) 2024-03-14 21:42:09 +03:00
Phorcys
de0813f37f fix(git-commit-signing): disable curl stderr output (#190) 2024-03-14 21:34:22 +03:00
Michael Brewer
d8fa7c959f feat(jetbrains-gateway): add rider support (#186) 2024-03-14 13:17:12 +03:00
Michael Brewer
c3d1b4125e doc: coder-server module does not have offline attribute (#180) 2024-03-11 11:53:50 +03:00
github-actions[bot]
472d80ade6 chore: bump version to 1.0.8 in README.md files (#182)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-03-07 00:34:50 +05:00
Florian Gareis
7d6c526146 Add app_name parameter to code-server (#176)
* Add `app_name` parameter to code-server

* Add quotes and use `display_name`
2024-03-06 17:10:03 +05:00
Michael Brewer
a3dc364227 feat: add order variable to coder_app modules (#177) 2024-03-05 21:15:37 +05:00
Florian Gareis
f335a62891 Add new nodejs module (#165)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-03-05 21:13:17 +05:00
Muhammad Atif Ali
8ed13be726 chore(vault-github): update README.md (#169) 2024-03-01 16:13:32 +05:00
github-actions[bot]
b90f6f9de8 chore: bump version to 1.0.7 in README.md files (#174)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-02-26 20:53:27 +05:00
Muhammad Atif Ali
948280600a fix(vault): fix version fetching logic (#172) 2024-02-26 20:51:45 +05:00
Muhammad Atif Ali
407738b2be feat(hcp-vault-secrets): add project_id variable to HCP provider (#173) 2024-02-26 20:50:42 +05:00
github-actions[bot]
08adb4a839 chore: bump version to 1.0.6 in README.md files (#171) 2024-02-23 23:52:36 +05:00
Muhammad Atif Ali
313ec59d46 Add terraform validation to linting (#170)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-23 23:49:47 +05:00
Muhammad Atif Ali
4b04d18f39 Add extensions support for vscode-web (#154)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-23 17:02:17 +05:00
github-actions[bot]
ee53ca0281 chore: bump version to 1.0.5 in README.md files (#168)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-02-21 12:09:01 +05:00
Muhammad Atif Ali
8e254a3bb9 docs: elaborate instructions for setting up hcp vault module (#163)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-19 13:44:07 +04:00
Muhammad Atif Ali
1ab53139b3 ci: fix ci permissions (#166) 2024-02-18 23:45:33 +05:00
Muhammad Atif Ali
147bea9782 bump version to v1.0.4 (#160) 2024-02-16 18:50:30 +03:00
Victor Urvantsev
8d8910c52a feat(jfrog): add option to customize server id for JFrog CLI (#158)
Co-authored-by: Victor Urvantsev <victoru@jfrog.com>
2024-02-16 13:16:14 +03:00
Florian Gareis
c00b7536cb Add slug to code server (#161) 2024-02-16 12:54:05 +03:00
Muhammad Atif Ali
d66d7e994e ci: set base branch for docs update PR (#155) 2024-02-14 20:11:13 +03:00
Muhammad Atif Ali
d10ce91a64 fix: fix fetching rc versions of vault cli (#156)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-14 17:04:56 +03:00
Muhammad Atif Ali
534491613f Update module versions to v1.0.3 (#159) 2024-02-14 16:37:01 +03:00
58 changed files with 1800 additions and 275 deletions

View File

@@ -9,7 +9,7 @@ on:
jobs:
update-readme:
permissions:
contents: read
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
@@ -33,6 +33,7 @@ jobs:
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 }}

5
.icons/desktop.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 6V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V6C1 4.35 2.35 3 4 3H28C29.65 3 31 4.35 31 6Z" fill="#2197F3"/>
<path d="M21 27H17V24C17 23.4478 16.5522 23 16 23C15.4478 23 15 23.4478 15 24V27H11C10.4478 27 10 27.4478 10 28C10 28.5522 10.4478 29 11 29H21C21.5522 29 22 28.5522 22 28C22 27.4478 21.5522 27 21 27Z" fill="#FFC10A"/>
<path d="M31 17V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V17H31Z" fill="#3F51B5"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

1
.icons/node.svg Normal file
View File

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -50,6 +50,12 @@ variable "mutable" {
description = "Whether the parameter is mutable."
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
@@ -69,9 +75,10 @@ resource "coder_app" "MODULE_NAME" {
slug = "MODULE_NAME"
display_name = "MODULE_NAME"
url = "http://localhost:${var.port}"
icon = loocal.icon_url
icon = local.icon_url
subdomain = false
share = "owner"
order = var.order
# Remove if the app does not have a healthcheck endpoint
healthcheck {

View File

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

View File

@@ -56,6 +56,14 @@ locals {
# frequently and including the `aws_regions` data source requires
# the provider, which requires a region.
regions = {
"af-south-1" = {
name = "Africa (Cape Town)"
icon = "/emojis/1f1ff-1f1e6.png"
}
"ap-east-1" = {
name = "Asia Pacific (Hong Kong)"
icon = "/emojis/1f1ed-1f1f0.png"
}
"ap-northeast-1" = {
name = "Asia Pacific (Tokyo)"
icon = "/emojis/1f1ef-1f1f5.png"
@@ -72,6 +80,10 @@ locals {
name = "Asia Pacific (Mumbai)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-south-2" = {
name = "Asia Pacific (Hyderabad)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-southeast-1" = {
name = "Asia Pacific (Singapore)"
icon = "/emojis/1f1f8-1f1ec.png"
@@ -80,18 +92,42 @@ locals {
name = "Asia Pacific (Sydney)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"ap-southeast-3" = {
name = "Asia Pacific (Jakarta)"
icon = "/emojis/1f1ee-1f1e9.png"
}
"ap-southeast-4" = {
name = "Asia Pacific (Melbourne)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"ca-central-1" = {
name = "Canada (Central)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"ca-west-1" = {
name = "Canada West (Calgary)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"eu-central-1" = {
name = "EU (Frankfurt)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-central-2" = {
name = "Europe (Zurich)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-north-1" = {
name = "EU (Stockholm)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-south-1" = {
name = "Europe (Milan)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-south-2" = {
name = "Europe (Spain)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-west-1" = {
name = "EU (Ireland)"
icon = "/emojis/1f1ea-1f1fa.png"
@@ -104,6 +140,14 @@ locals {
name = "EU (Paris)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"il-central-1" = {
name = "Israel (Tel Aviv)"
icon = "/emojis/1f1ee-1f1f1.png"
}
"me-south-1" = {
name = "Middle East (Bahrain)"
icon = "/emojis/1f1e7-1f1ed.png"
}
"sa-east-1" = {
name = "South America (São Paulo)"
icon = "/emojis/1f1e7-1f1f7.png"
@@ -145,4 +189,4 @@ data "coder_parameter" "region" {
output "value" {
value = data.coder_parameter.region.value
}
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
}
```
@@ -28,7 +28,7 @@ module "code-server" {
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
@@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -74,20 +74,34 @@ Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
### Offline Mode
### Offline and Use Cached Modes
By default the module looks for code-server at `/tmp/code-server` but this can be changed with `install_prefix`.
Run an existing copy of code-server if found, otherwise download from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
offline = true
}

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("code-server", async () => {
await runTerraformInit(import.meta.dir);
@@ -8,5 +12,27 @@ describe("code-server", async () => {
agent_id: "foo",
});
it("use_cached and offline can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
use_cached: "true",
offline: "true",
});
};
expect(t).toThrow("Offline and Use Cached can not be used together");
});
it("offline and extensions can not be used together", () => {
const t = async () => {
await runTerraformApply(import.meta.dir, {
agent_id: "foo",
offline: "true",
extensions: '["1", "2"]',
});
};
expect(t).toThrow("Offline mode does not allow extensions to be installed");
});
// More tests depend on shebang refactors
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -32,6 +32,12 @@ variable "display_name" {
default = "code-server"
}
variable "slug" {
type = string
description = "The slug for the code-server application."
default = "code-server"
}
variable "settings" {
type = map(string)
description = "A map of settings to apply to code-server."
@@ -71,6 +77,24 @@ variable "share" {
}
}
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 "offline" {
type = bool
description = "Just run code-server in the background, don't fetch it from GitHub"
default = false
}
variable "use_cached" {
type = bool
description = "Uses cached copy code-server in the background, otherwise fetched it from GitHub"
default = false
}
resource "coder_script" "code-server" {
agent_id = var.agent_id
display_name = "code-server"
@@ -78,23 +102,39 @@ resource "coder_script" "code-server" {
script = templatefile("${path.module}/run.sh", {
VERSION : var.install_version,
EXTENSIONS : join(",", var.extensions),
APP_NAME : var.display_name,
PORT : var.port,
LOG_PATH : var.log_path,
INSTALL_PREFIX : var.install_prefix,
// This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
})
run_on_start = true
lifecycle {
precondition {
condition = !var.offline || length(var.extensions) == 0
error_message = "Offline mode does not allow extensions to be installed"
}
precondition {
condition = !var.offline || !var.use_cached
error_message = "Offline and Use Cached can not be used together"
}
}
}
resource "coder_app" "code-server" {
agent_id = var.agent_id
slug = "code-server"
slug = var.slug
display_name = var.display_name
url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}"
icon = "/icon/code.svg"
subdomain = false
share = var.share
order = var.order
healthcheck {
url = "http://localhost:${var.port}/healthz"

View File

@@ -4,6 +4,34 @@ EXTENSIONS=("${EXTENSIONS}")
BOLD='\033[0;1m'
CODE='\033[36;40;1m'
RESET='\033[0m'
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
function run_code_server() {
echo "👷 Running code-server in the background..."
echo "Check logs at ${LOG_PATH}!"
$CODE_SERVER --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
}
# Check if the settings file exists...
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
echo "⚙️ Creating settings file..."
mkdir -p ~/.local/share/code-server/User
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
fi
# Check if code-server is already installed for offline or cached mode
if [ -f "$CODE_SERVER" ]; then
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
echo "🥳 Found a copy of code-server"
run_code_server
exit 0
fi
fi
# Offline mode always expects a copy of code-server to be present
if [ "${OFFLINE}" = true ]; then
echo "Failed to find a copy of code-server"
exit 1
fi
printf "$${BOLD}Installing code-server!\n"
@@ -22,8 +50,6 @@ if [ $? -ne 0 ]; then
fi
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
for extension in "$${EXTENSIONLIST[@]}"; do
@@ -38,13 +64,4 @@ for extension in "$${EXTENSIONLIST[@]}"; do
fi
done
# Check if the settings file exists...
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
echo "⚙️ Creating settings file..."
mkdir -p ~/.local/share/code-server/User
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
fi
echo "👷 Running code-server in the background..."
echo "Check logs at ${LOG_PATH}!"
$CODE_SERVER --auth none --port ${PORT} > ${LOG_PATH} 2>&1 &
run_code_server

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -52,6 +52,12 @@ variable "share" {
}
}
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" {
agent_id = var.agent_id
display_name = "File Browser"
@@ -74,4 +80,5 @@ resource "coder_app" "filebrowser" {
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
subdomain = true
share = var.share
order = var.order
}

View File

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

View File

@@ -16,7 +16,7 @@ variable "agent_id" {
resource "coder_script" "git-commit-signing" {
display_name = "Git commit signing"
icon = "https://raw.githubusercontent.com/coder/modules/main/.icons/git.svg"
icon = "/icon/git.svg"
script = file("${path.module}/run.sh")
run_on_start = true

View File

@@ -21,7 +21,8 @@ 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}")
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}" \
--silent --show-error)
jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
$ssh_key

View File

@@ -14,16 +14,25 @@ This module lets you fetch all or selective secrets from a [HCP Vault Secrets](h
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.3"
agent_id = coder_agent.example.id
app_name = "demo-app"
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, you must 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. See the [HCP Vault Secrets documentation](https://developer.hashicorp.com/hcp/docs/vault-secrets) for more information.
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
@@ -31,10 +40,11 @@ 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.3"
agent_id = coder_agent.example.id
app_name = "demo-app"
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"
}
```
@@ -44,11 +54,12 @@ To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` inp
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.3"
agent_id = coder_agent.example.id
app_name = "demo-app"
secrets = ["MY_SECRET_1", "MY_SECRET_2"]
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"]
}
```
@@ -59,9 +70,10 @@ Set `client_id` and `client_secret` as module inputs.
```tf
module "vault" {
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
version = "1.0.3"
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

@@ -16,6 +16,7 @@ terraform {
provider "hcp" {
client_id = var.client_id
client_secret = var.client_secret
project_id = var.project_id
}
provider "coder" {}
@@ -25,6 +26,11 @@ variable "agent_id" {
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

View File

@@ -14,12 +14,12 @@ This module adds a JetBrains Gateway Button to open any workspace with a single
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.2"
version = "1.0.9"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS", "IU", "PY", "PS", "CL", "RM"]
default = "PY"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
default = "GO"
}
```
@@ -32,7 +32,7 @@ module "jetbrains_gateway" {
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.2"
version = "1.0.9"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
@@ -52,3 +52,4 @@ This module and JetBrains Gateway support the following JetBrains IDEs:
- PhpStorm (`PS`)
- CLion (`CL`)
- RubyMine (`RM`)
- Rider (`RD`)

View File

@@ -11,18 +11,16 @@ describe("jetbrains-gateway", async () => {
await testRequiredVariables(import.meta.dir, {
agent_id: "foo",
agent_name: "foo",
folder: "/baz/",
folder: "/home/foo",
});
it("default to first ide", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "foo",
folder: "/baz/",
folder: "/home/foo",
jetbrains_ides: '["IU", "GO", "PY"]',
});
expect(state.outputs.jetbrains_ides.value).toBe(
'["IU","232.10203.10","https://download.jetbrains.com/idea/ideaIU-2023.2.4.tar.gz"]',
);
expect(state.outputs.identifier.value).toBe("IU");
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.11"
version = ">= 0.17"
}
}
}
@@ -22,6 +22,10 @@ variable "agent_name" {
variable "folder" {
type = string
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" {
@@ -30,8 +34,16 @@ variable "default" {
description = "Default IDE"
}
locals {
supported_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM"]
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 "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
variable "jetbrains_ide_versions" {
@@ -42,56 +54,59 @@ variable "jetbrains_ide_versions" {
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "232.10203.10"
version = "2023.2.4"
build_number = "233.14808.21"
version = "2023.3.5"
}
"PS" = {
build_number = "232.10072.32"
version = "2023.2.3"
build_number = "233.14808.18"
version = "2023.3.5"
}
"WS" = {
build_number = "232.10203.14"
version = "2023.2.4"
build_number = "233.14475.40"
version = "2023.3.4"
}
"PY" = {
build_number = "232.10203.26"
version = "2023.2.4"
build_number = "233.14475.56"
version = "2023.3.4"
}
"CL" = {
build_number = "232.9921.42"
version = "2023.2.2"
build_number = "233.14475.31"
version = "2023.3.4"
}
"GO" = {
build_number = "232.10203.20"
version = "2023.2.4"
build_number = "233.14808.20"
version = "2023.3.5"
}
"RM" = {
build_number = "232.10203.15"
version = "2023.2.4"
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 var.jetbrains_ide_versions : contains(local.supported_ides, code)
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(",", local.supported_ides)}."
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" {
type = list(string)
description = "The list of IDE product codes."
default = local.supported_ides
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
validation {
condition = (
alltrue([
for code in var.jetbrains_ides : contains(local.supported_ides, code)
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
])
)
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", local.supported_ides)}."
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"])}."
}
# check if the list is empty
validation {
@@ -108,58 +123,79 @@ variable "jetbrains_ides" {
locals {
jetbrains_ides = {
"GO" = {
icon = "/icon/goland.svg",
name = "GoLand",
value = jsonencode(["GO", var.jetbrains_ide_versions["GO"].build_number, "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"])
icon = "/icon/goland.svg",
name = "GoLand",
identifier = "GO",
build_number = var.jetbrains_ide_versions["GO"].build_number,
download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
},
"WS" = {
icon = "/icon/webstorm.svg",
name = "WebStorm",
value = jsonencode(["WS", var.jetbrains_ide_versions["WS"].build_number, "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"])
icon = "/icon/webstorm.svg",
name = "WebStorm",
identifier = "WS",
build_number = var.jetbrains_ide_versions["WS"].build_number,
download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
},
"IU" = {
icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Ultimate",
value = jsonencode(["IU", var.jetbrains_ide_versions["IU"].build_number, "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"])
icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Ultimate",
identifier = "IU",
build_number = var.jetbrains_ide_versions["IU"].build_number,
download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
},
"PY" = {
icon = "/icon/pycharm.svg",
name = "PyCharm Professional",
value = jsonencode(["PY", var.jetbrains_ide_versions["PY"].build_number, "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"])
icon = "/icon/pycharm.svg",
name = "PyCharm Professional",
identifier = "PY",
build_number = var.jetbrains_ide_versions["PY"].build_number,
download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
},
"CL" = {
icon = "/icon/clion.svg",
name = "CLion",
value = jsonencode(["CL", var.jetbrains_ide_versions["CL"].build_number, "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"])
icon = "/icon/clion.svg",
name = "CLion",
identifier = "CL",
build_number = var.jetbrains_ide_versions["CL"].build_number,
download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
},
"PS" = {
icon = "/icon/phpstorm.svg",
name = "PhpStorm",
value = jsonencode(["PS", var.jetbrains_ide_versions["PS"].build_number, "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"])
icon = "/icon/phpstorm.svg",
name = "PhpStorm",
identifier = "PS",
build_number = var.jetbrains_ide_versions["PS"].build_number,
download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
},
"RM" = {
icon = "/icon/rubymine.svg",
name = "RubyMine",
value = jsonencode(["RM", var.jetbrains_ide_versions["RM"].build_number, "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"])
icon = "/icon/rubymine.svg",
name = "RubyMine",
identifier = "RM",
build_number = var.jetbrains_ide_versions["RM"].build_number,
download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
}
"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" {
type = "list(string)"
type = "string"
name = "jetbrains_ide"
display_name = "JetBrains IDE"
icon = "/icon/gateway.svg"
mutable = true
# 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
default = var.default == "" ? var.jetbrains_ides[0] : var.default
order = var.coder_parameter_order
dynamic "option" {
for_each = { for key, value in local.jetbrains_ides : key => value if contains(var.jetbrains_ides, key) }
for_each = var.jetbrains_ides
content {
icon = option.value.icon
name = option.value.name
value = option.value.value
icon = lookup(local.jetbrains_ides, option.value).icon
name = lookup(local.jetbrains_ides, option.value).name
value = lookup(local.jetbrains_ides, option.value).identifier
}
}
}
@@ -168,10 +204,11 @@ data "coder_workspace" "me" {}
resource "coder_app" "gateway" {
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"
icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon
display_name = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE")
icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg")
external = true
order = var.order
url = join("", [
"jetbrains-gateway://connect#type=coder&workspace=",
data.coder_workspace.me.name,
@@ -184,14 +221,38 @@ resource "coder_app" "gateway" {
"&token=",
"$SESSION_TOKEN",
"&ide_product_code=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[0],
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier,
"&ide_build_number=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[1],
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number,
"&ide_download_link=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[2],
local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
])
}
output "jetbrains_ides" {
output "identifier" {
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

@@ -17,7 +17,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.2"
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"
@@ -44,7 +44,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.2"
version = "1.0.5"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
@@ -72,7 +72,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.2"
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"

View File

@@ -19,6 +19,12 @@ variable "jfrog_url" {
}
}
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."
@@ -79,6 +85,7 @@ resource "coder_script" "jfrog" {
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,

View File

@@ -15,9 +15,9 @@ fi
# flows.
export CI=true
# Authenticate JFrog CLI with Artifactory.
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite 0
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 0
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then

View File

@@ -15,7 +15,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -41,7 +41,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
@@ -74,7 +74,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -87,6 +87,26 @@ module "jfrog" {
}
```
### Add a custom token description
```tf
data "coder_workspace" "me" {}
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
token_description = "Token for Coder workspace: ${data.coder_workspace.me.owner}/${data.coder_workspace.me.name}"
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).

View File

@@ -23,11 +23,23 @@ variable "jfrog_url" {
}
}
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 "token_description" {
type = string
description = "Free text token description. Useful for filtering and managing tokens."
default = "Token for Coder workspace"
}
variable "check_license" {
type = bool
description = "Toggle for pre-flight checking of Artifactory license. Default to `true`."
@@ -101,6 +113,7 @@ resource "artifactory_scoped_token" "me" {
scopes = ["applied-permissions/user"]
refreshable = var.refreshable
expires_in = var.expires_in
description = var.token_description
}
data "coder_workspace" "me" {}
@@ -112,6 +125,7 @@ resource "coder_script" "jfrog" {
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,

View File

@@ -15,9 +15,9 @@ fi
# flows.
export CI=true
# Authenticate JFrog CLI with Artifactory.
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite 0
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 0
jf c use "${JFROG_SERVER_ID}"
# Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then

View File

@@ -16,7 +16,7 @@ A module that adds Jupyter Notebook in your Coder template.
```tf
module "jupyter-notebook" {
source = "registry.coder.com/modules/jupyter-notebook/coder"
version = "1.0.2"
version = "1.0.8"
agent_id = coder_agent.example.id
}
```

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -36,6 +36,12 @@ variable "share" {
}
}
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" {
agent_id = var.agent_id
display_name = "jupyter-notebook"
@@ -55,4 +61,5 @@ resource "coder_app" "jupyter-notebook" {
icon = "/icon/jupyter.svg"
subdomain = true
share = var.share
order = var.order
}

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -36,6 +36,12 @@ variable "share" {
}
}
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" {
agent_id = var.agent_id
display_name = "jupyterlab"
@@ -55,4 +61,5 @@ resource "coder_app" "jupyterlab" {
icon = "/icon/jupyter.svg"
subdomain = true
share = var.share
order = var.order
}

58
nodejs/README.md Normal file
View File

@@ -0,0 +1,58 @@
---
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.10"
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.10"
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.10"
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"
}
```

12
nodejs/main.test.ts Normal file
View File

@@ -0,0 +1,12 @@
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
});

52
nodejs/main.tf Normal file
View File

@@ -0,0 +1,52 @@
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 (relative to $HOME)."
default = ".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
}

51
nodejs/run.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/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="$HOME/$${INSTALL_PREFIX}/nvm"
mkdir -p "$NVM_DIR"
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

263
package-lock.json generated Normal file
View File

@@ -0,0 +1,263 @@
{
"name": "modules",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "modules",
"devDependencies": {
"bun-types": "^1.0.18",
"gray-matter": "^4.0.3",
"marked": "^12.0.0",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.3.3"
}
},
"node_modules/@types/node": {
"version": "20.12.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
"integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/bun-types": {
"version": "1.1.16",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz",
"integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==",
"dev": true,
"dependencies": {
"@types/node": "~20.12.8",
"@types/ws": "~8.5.10"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mvdan-sh": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
"dev": true
},
"node_modules/prettier": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-sh": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
"dev": true,
"dependencies": {
"mvdan-sh": "^0.10.1",
"sh-syntax": "^0.4.1"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/prettier-plugin-terraform-formatter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
"dev": true,
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/sh-syntax": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
"dev": true,
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true
},
"node_modules/typescript": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -4,13 +4,13 @@
"test": "bun test",
"fmt": "bun x prettier -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf",
"fmt:ci": "bun x prettier --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.tf .sample/main.tf",
"lint": "bun run lint.ts",
"lint": "bun run lint.ts && ./terraform_validate.sh",
"update-version": "./update-version.sh"
},
"devDependencies": {
"bun-types": "^1.0.18",
"gray-matter": "^4.0.3",
"marked": "^11.1.0",
"marked": "^12.0.0",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},

29
terraform_validate.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/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

73
test.ts
View File

@@ -29,8 +29,10 @@ export const runContainer = async (
return containerID.trim();
};
// executeScriptInContainer finds the only "coder_script"
// resource in the given state and runs it in a container.
/**
* Finds the only "coder_script" resource in the given state and runs it in a
* container.
*/
export const executeScriptInContainer = async (
state: TerraformState,
image: string,
@@ -76,27 +78,22 @@ export const execContainer = async (
};
};
type TerraformStateResource = {
type: string;
name: string;
provider: string;
instances: [{ attributes: Record<string, any> }];
};
export interface TerraformState {
outputs: {
[key: string]: {
type: string;
value: any;
};
}
resources: [
{
type: string;
name: string;
provider: string;
instances: [
{
attributes: {
[key: string]: any;
};
},
];
},
];
};
resources: [TerraformStateResource, ...TerraformStateResource[]];
}
export interface CoderScriptAttributes {
@@ -105,10 +102,11 @@ export interface CoderScriptAttributes {
url: string;
}
// findResourceInstance finds the first instance of the given resource
// type in the given state. If name is specified, it will only find
// the instance with the given name.
export const findResourceInstance = <T extends "coder_script" | string>(
/**
* finds the first instance of the given resource type in the given state. If
* name is specified, it will only find the instance with the given name.
*/
export const findResourceInstance = <T extends string>(
state: TerraformState,
type: T,
name?: string,
@@ -131,12 +129,13 @@ export const findResourceInstance = <T extends "coder_script" | string>(
return resource.instances[0].attributes as any;
};
// testRequiredVariables creates a test-case
// for each variable provided and ensures that
// the apply fails without it.
export const testRequiredVariables = (
/**
* Creates a test-case for each variable provided and ensures that the apply
* fails without it.
*/
export const testRequiredVariables = <TVars extends Record<string, string>>(
dir: string,
vars: Record<string, string>,
vars: TVars,
) => {
// Ensures that all required variables are provided.
it("required variables", async () => {
@@ -165,12 +164,16 @@ export const testRequiredVariables = (
});
};
// runTerraformApply runs terraform apply in the given directory
// with the given variables. It is fine to run in parallel with
// other instances of this function, as it uses a random state file.
export const runTerraformApply = async (
/**
* Runs terraform apply in the given directory with the given variables. It is
* fine to run in parallel with other instances of this function, as it uses a
* random state file.
*/
export const runTerraformApply = async <
TVars extends Readonly<Record<string, string>>,
>(
dir: string,
vars: Record<string, string>,
vars: TVars,
): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
@@ -203,7 +206,9 @@ export const runTerraformApply = async (
return JSON.parse(content);
};
// runTerraformInit runs terraform init in the given directory.
/**
* Runs terraform init in the given directory.
*/
export const runTerraformInit = async (dir: string) => {
const proc = spawn(["terraform", "init"], {
cwd: dir,
@@ -221,5 +226,5 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
"Content-Type": "application/json",
},
status: statusCode,
})
}
});
};

View File

@@ -8,14 +8,7 @@ set -euo pipefail
current_tag=$(git describe --tags --abbrev=0)
previous_tag=$(git describe --tags --abbrev=0 $current_tag^)
mapfile -t changed_files < <(git diff --name-only "$previous_tag" "$current_tag" | xargs dirname | sort -u | grep -v '^\.')
changed_dirs=()
for file in $changed_files; do
dir=$(dirname "$file")
changed_dirs+=("$dir")
done
changed_dirs=($(printf "%s\n" "${changed_dirs[@]}" | sort -u))
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 $?
@@ -33,4 +26,4 @@ for dir in "${changed_dirs[@]}"; do
}
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
fi
done
done

View File

@@ -15,7 +15,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.3"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
}
@@ -24,13 +24,13 @@ module "vault" {
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
```shell
vault kv get -mount=coder my-secret
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/data/my-secret"
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
```
![Vault login](../.images/vault-login.png)
@@ -46,7 +46,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.3"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
@@ -58,7 +58,7 @@ module "vault" {
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.3"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id"
@@ -71,7 +71,7 @@ module "vault" {
```tf
module "vault" {
source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.3"
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,7 +1,7 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test";
describe("vault-token", async () => {
describe("vault-github", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {

View File

@@ -32,9 +32,19 @@ unzip_safe() {
}
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 -oP 'vault/\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n 1)
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"
@@ -60,7 +70,7 @@ install() {
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"
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

View File

@@ -21,7 +21,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/modules/vault-token/coder"
version = "1.0.3"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_token = var.token
vault_addr = "https://vault.example.com"
@@ -74,7 +74,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/modules/vault-token/coder"
version = "1.0.3"
version = "1.0.7"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token

View File

@@ -30,9 +30,19 @@ unzip_safe() {
}
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 -oP 'vault/\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n 1)
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"

View File

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

View File

@@ -20,5 +20,18 @@ describe("vscode-desktop", async () => {
expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
);
const resources = 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 = state.resources;
expect(resources[1].instances[0].attributes.order).toBe(22);
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -20,6 +20,12 @@ variable "folder" {
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" {}
resource "coder_app" "vscode" {
@@ -28,6 +34,7 @@ resource "coder_app" "vscode" {
icon = "/icon/code.svg"
slug = "vscode"
display_name = "VS Code Desktop"
order = var.order
url = var.folder != "" ? join("", [
"vscode://coder.coder-remote/open?owner=",
data.coder_workspace.me.owner,

View File

@@ -9,12 +9,12 @@ tags: [helper, ide, vscode, web]
# VS Code Web
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.
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.
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -29,10 +29,39 @@ module "vscode-web" {
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.2"
version = "1.0.10"
agent_id = coder_agent.example.id
install_dir = "/home/coder/.vscode-web"
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
accept_license = true
}
```
### Install Extensions
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
}
```
### Pre-configure Settings
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
accept_license = true
}
```

View File

@@ -1,63 +0,0 @@
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 {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -53,15 +53,21 @@ variable "log_path" {
default = "/tmp/vscode-web.log"
}
variable "install_dir" {
variable "install_prefix" {
type = string
description = "The directory to install VS Code CLI"
default = "/tmp/vscode-cli"
description = "The prefix to install vscode-web to."
default = "/tmp/vscode-web"
}
variable "extensions" {
type = list(string)
description = "A list of extensions to install."
default = []
}
variable "accept_license" {
type = bool
description = "Accept the VS Code license. https://code.visualstudio.com/license"
description = "Accept the VS Code Server license. https://code.visualstudio.com/license/server"
default = false
validation {
condition = var.accept_license == true
@@ -69,6 +75,28 @@ 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
}
variable "settings" {
type = map(string)
description = "A map of settings to apply to VS Code web."
default = {}
}
resource "coder_script" "vscode-web" {
agent_id = var.agent_id
display_name = "VS Code Web"
@@ -76,7 +104,11 @@ resource "coder_script" "vscode-web" {
script = templatefile("${path.module}/run.sh", {
PORT : var.port,
LOG_PATH : var.log_path,
INSTALL_DIR : var.install_dir,
INSTALL_PREFIX : var.install_prefix,
EXTENSIONS : join(",", var.extensions),
TELEMETRY_LEVEL : var.telemetry_level,
// This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
})
run_on_start = true
}
@@ -89,6 +121,7 @@ resource "coder_app" "vscode-web" {
icon = "/icon/code.svg"
subdomain = true
share = var.share
order = var.order
healthcheck {
url = "http://localhost:${var.port}/healthz"

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

@@ -1,21 +1,56 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
BOLD='\033[0;1m'
EXTENSIONS=("${EXTENSIONS}")
# Create install directory if it doesn't exist
mkdir -p ${INSTALL_DIR}
# Create install prefix
mkdir -p ${INSTALL_PREFIX}
printf "$${BOLD}Installing vscode-cli!\n"
printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n"
# Download and extract code-cli tarball
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)
# Download and extract vscode-server
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
*)
echo "Unsupported architecture"
exit 1
;;
esac
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2)
output=$(curl -fsSL 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
echo "Failed to install vscode-cli: $output"
echo "Failed to install Microsoft Visual Studio Code Server: $output"
exit 1
fi
printf "🥳 vscode-cli has been installed.\n\n"
printf "$${BOLD}Microsoft Visual Studio Code Server has been installed.\n"
echo "👷 Running ${INSTALL_DIR}/bin/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms in the background..."
VSCODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# 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
# Check if the settings file exists...
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
echo "⚙️ Creating settings file..."
mkdir -p ~/.vscode-server/data/Machine
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
fi
echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
${INSTALL_DIR}/code serve-web --port ${PORT} --without-connection-token --accept-server-license-terms > ${LOG_PATH} 2>&1 &
"${INSTALL_PREFIX}/bin/code-server" serve-local --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &

35
windows-rdp/README.md Normal file
View File

@@ -0,0 +1,35 @@
---
display_name: Windows RDP
description: RDP Server and Web Client powered by Devolutions
icon: ../.icons/desktop.svg
maintainer_github: coder
verified: false
tags: [windows, rdp, web, desktop]
---
# Windows RDP
Enable Remote Desktop + a web based client on Windows workspaces, powered by [devolutions-gateway](https://github.com/Devolutions/devolutions-gateway)
[![Web RDP on Windows](https://cdn.loom.com/sessions/thumbnails/a5d98c7007a7417fb572aba1acf8d538-with-play.gif)](https://www.loom.com/share/a5d98c7007a7417fb572aba1acf8d538)
## Usage
```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "github.com/coder/modules//windows-rdp?ref=web-rdp"
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
}
```
## Tested on
- ✅ GCP with Windows Server 2022: [Example template](https://gist.github.com/bpmct/18918b8cab9f20295e5c4039b92b5143)
## Roadmap
- [ ] Test on additional cloud providers
- [ ] Automatically establish web RDP session when users click "web RDP"
> This may require forking [the webapp from devolutions-gateway](https://github.com/Devolutions/devolutions-gateway/tree/master/webapp), modifying `webapp/`, building, and specifying a new [static root path](https://github.com/Devolutions/devolutions-gateway/blob/a884cbb8ff313496fb3d4072e67ef75350c40c03/devolutions-gateway/tests/config.rs#L271). Ideally we can upstream this functionality.

View File

@@ -0,0 +1,410 @@
// @ts-check
/**
* @file Defines the custom logic for patching in UI changes/behavior into the
* base Devolutions Gateway Angular app.
*
* Defined as a JS file to remove the need to have a separate compilation step.
* It is highly recommended that you work on this file from within VS Code so
* that you can take advantage of the @ts-check directive and get some type-
* checking still.
*
* Other notes about the weird ways this file is set up:
* - A lot of the HTML selectors in this file will look nonstandard. This is
* because they are actually custom Angular components.
* - It is strongly advised that you avoid template literals that use the
* placeholder syntax via the dollar sign. The Terraform script looks for
* these characters so that it can inject Coder-specific values, so any
* template literal that uses the character actually needs to double up each
* of them. There are already a few places in this file where it couldn't be
* avoided, but avoiding this as much as possible will save you some headache.
* - All the CSS should be written via custom style tags and the !important
* directive (as much as that is a bad idea most of the time). We do not
* control the Angular app, so we have to modify things from afar to ensure
* that as Angular's internal state changes, it doesn't modify its HTML nodes
* in a way that causes our custom styles to get wiped away.
*
* @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry
* @typedef {Readonly<Record<string, FormFieldEntry>>} FormFieldEntries
*/
/**
* The communication protocol to set Devolutions to.
*/
const PROTOCOL = "RDP";
/**
* The hostname to use with Devolutions.
*/
const HOSTNAME = "localhost";
/**
* How often to poll the screen for the main Devolutions form.
*/
const SCREEN_POLL_INTERVAL_MS = 500;
/**
* The fields in the Devolutions sign-in form that should be populated with
* values from the Coder workspace.
*
* All properties should be defined as placeholder templates in the form
* VALUE_NAME. The Coder module, when spun up, should then run some logic to
* replace the template slots with actual values. These values should never
* change from within JavaScript itself.
*
* @satisfies {FormFieldEntries}
*/
const formFieldEntries = {
/** @readonly */
username: {
/** @readonly */
querySelector: "web-client-username-control input",
/** @readonly */
value: "${CODER_USERNAME}",
},
/** @readonly */
password: {
/** @readonly */
querySelector: "web-client-password-control input",
/** @readonly */
value: "${CODER_PASSWORD}",
},
};
/**
* Handles typing in the values for the input form. All values are written
* immediately, even though that would be physically impossible with a real
* keyboard.
*
* Note: this code will never break, but you might get warnings in the console
* from Angular about unexpected value changes. Angular patches over a lot of
* the built-in browser APIs to support its component change detection system.
* As part of that, it has validations for checking whether an input it
* previously had control over changed without it doing anything.
*
* But the only way to simulate a keyboard input is by setting the input's
* .value property, and then firing an input event. So basically, the inner
* value will change, which Angular won't be happy about, but then the input
* event will fire and sync everything back together.
*
* @param {HTMLInputElement} inputField
* @param {string} inputText
* @returns {Promise<void>}
*/
function setInputValue(inputField, inputText) {
return new Promise((resolve, reject) => {
// Adding timeout for input event, even though we'll be dispatching it
// immediately, just in the off chance that something in the Angular app
// intercepts it or stops it from propagating properly
const timeoutId = window.setTimeout(() => {
reject(new Error("Input event did not get processed correctly in time."));
}, 3_000);
const handleSuccessfulDispatch = () => {
window.clearTimeout(timeoutId);
inputField.removeEventListener("input", handleSuccessfulDispatch);
resolve();
};
inputField.addEventListener("input", handleSuccessfulDispatch);
// Code assumes that Angular will have an event handler in place to handle
// the new event
const inputEvent = new Event("input", {
bubbles: true,
cancelable: true,
});
inputField.value = inputText;
inputField.dispatchEvent(inputEvent);
});
}
/**
* Takes a Devolutions remote session form, auto-fills it with data, and then
* submits it.
*
* The logic here is more convoluted than it should be for two main reasons:
* 1. Devolutions' HTML markup has errors. There are labels, but they aren't
* bound to the inputs they're supposed to describe. This means no easy hooks
* for selecting the elements, unfortunately.
* 2. Trying to modify the .value properties on some of the inputs doesn't
* work. Probably some combo of Angular data-binding and some inputs having
* the readonly attribute. Have to simulate user input to get around this.
*
* @param {HTMLFormElement} myForm
* @returns {Promise<void>}
*/
async function autoSubmitForm(myForm) {
const setProtocolValue = () => {
/** @type {HTMLDivElement | null} */
const protocolDropdownTrigger = myForm.querySelector('div[role="button"]');
if (protocolDropdownTrigger === null) {
throw new Error("No clickable trigger for setting protocol value");
}
protocolDropdownTrigger.click();
// Can't use form as container for querying the list of dropdown options,
// because the elements don't actually exist inside the form. They're placed
// in the top level of the HTML doc, and repositioned to make it look like
// they're part of the form. Avoids CSS stacking context issues, maybe?
/** @type {HTMLLIElement | null} */
const protocolOption = document.querySelector(
'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li',
);
if (protocolOption === null) {
throw new Error(
"Unable to find protocol option on screen that matches desired protocol",
);
}
protocolOption.click();
};
const setHostname = () => {
/** @type {HTMLInputElement | null} */
const hostnameInput = myForm.querySelector("p-autocomplete#hostname input");
if (hostnameInput === null) {
throw new Error("Unable to find field for adding hostname");
}
return setInputValue(hostnameInput, HOSTNAME);
};
const setCoderFormFieldValues = async () => {
// The RDP form will not appear on screen unless the dropdown is set to use
// the RDP protocol
const rdpSubsection = myForm.querySelector("rdp-form");
if (rdpSubsection === null) {
throw new Error(
"Unable to find RDP subsection. Is the value of the protocol set to RDP?",
);
}
for (const { value, querySelector } of Object.values(formFieldEntries)) {
/** @type {HTMLInputElement | null} */
const input = document.querySelector(querySelector);
if (input === null) {
throw new Error(
'Unable to element that matches query "' + querySelector + '"',
);
}
await setInputValue(input, value);
}
};
const triggerSubmission = () => {
/** @type {HTMLButtonElement | null} */
const submitButton = myForm.querySelector(
'p-button[ng-reflect-type="submit"] button',
);
if (submitButton === null) {
throw new Error("Unable to find submission button");
}
if (submitButton.disabled) {
throw new Error(
"Unable to submit form because submit button is disabled. Are all fields filled out correctly?",
);
}
submitButton.click();
};
setProtocolValue();
await setHostname();
await setCoderFormFieldValues();
triggerSubmission();
}
/**
* Sets up logic for auto-populating the form data when the form appears on
* screen.
*
* @returns {void}
*/
function setupFormDetection() {
/** @type {HTMLFormElement | null} */
let formValueFromLastMutation = null;
/** @returns {void} */
const onDynamicTabMutation = () => {
/** @type {HTMLFormElement | null} */
const latestForm = document.querySelector("web-client-form > form");
// Only try to auto-fill if we went from having no form on screen to
// having a form on screen. That way, we don't accidentally override the
// form if the user is trying to customize values, and this essentially
// makes the script values function as default values
const mounted = formValueFromLastMutation === null && latestForm !== null;
if (mounted) {
autoSubmitForm(latestForm);
}
formValueFromLastMutation = latestForm;
};
/** @type {number | undefined} */
let pollingId = undefined;
/** @returns {void} */
const checkScreenForDynamicTab = () => {
const dynamicTab = document.querySelector("web-client-dynamic-tab");
// Keep polling until the main content container is on screen
if (dynamicTab === null) {
return;
}
window.clearInterval(pollingId);
// Call the mutation callback manually, to ensure it runs at least once
onDynamicTabMutation();
// Having the mutation observer is kind of an extra safety net that isn't
// really expected to run that often. Most of the content in the dynamic
// tab is being rendered through Canvas, which won't trigger any mutations
// that the observer can detect
const dynamicTabObserver = new MutationObserver(onDynamicTabMutation);
dynamicTabObserver.observe(dynamicTab, {
subtree: true,
childList: true,
});
};
pollingId = window.setInterval(
checkScreenForDynamicTab,
SCREEN_POLL_INTERVAL_MS,
);
}
/**
* Sets up custom styles for hiding default Devolutions elements that Coder
* users shouldn't need to care about.
*
* @returns {void}
*/
function setupAlwaysOnStyles() {
const styleId = "coder-patch--styles-always-on";
const existingContainer = document.querySelector("#" + styleId);
if (existingContainer) {
return;
}
const styleContainer = document.createElement("style");
styleContainer.id = styleId;
styleContainer.innerHTML = `
/* app-menu corresponds to the sidebar of the default view. */
app-menu {
display: none !important;
}
`;
document.head.appendChild(styleContainer);
}
function hideFormForInitialSubmission() {
const styleId = "coder-patch--styles-initial-submission";
const cssOpacityVariableName = "--coder-opacity-multiplier";
/** @type {HTMLStyleElement | null} */
let styleContainer = document.querySelector("#" + styleId);
if (!styleContainer) {
styleContainer = document.createElement("style");
styleContainer.id = styleId;
styleContainer.innerHTML = `
/*
Have to use opacity instead of visibility, because the element still
needs to be interactive via the script so that it can be auto-filled.
*/
:root {
/*
Can be 0 or 1. Start off invisible to avoid risks of UI flickering,
but the rest of the function should be in charge of making the form
container visible again if something goes wrong during setup.
Double dollar sign needed to avoid Terraform script false positives
*/
$${cssOpacityVariableName}: 0;
}
/*
web-client-form is the container for the main session form, while
the div is for the dropdown that is used for selecting the protocol.
The dropdown is not inside of the form for CSS styling reasons, so we
need to select both.
*/
web-client-form,
body > div.p-overlay {
/*
Double dollar sign needed to avoid Terraform script false positives
*/
opacity: calc(100% * var($${cssOpacityVariableName})) !important;
}
`;
document.head.appendChild(styleContainer);
}
// The root node being undefined should be physically impossible (if it's
// undefined, the browser itself is busted), but we need to do a type check
// here so that the rest of the function doesn't need to do type checks over
// and over.
const rootNode = document.querySelector(":root");
if (!(rootNode instanceof HTMLHtmlElement)) {
// Remove the container entirely because if the browser is busted, who knows
// if the CSS variables can be applied correctly. Better to have something
// be a bit more ugly/painful to use, than have it be impossible to use
styleContainer.remove();
return;
}
// It's safe to make the form visible preemptively because Devolutions
// outputs the Windows view through an HTML canvas that it overlays on top
// of the rest of the app. Even if the form isn't hidden at the style level,
// it will still be covered up.
const restoreOpacity = () => {
rootNode.style.setProperty(cssOpacityVariableName, "1");
};
// If this file gets more complicated, it might make sense to set up the
// timeout and event listener so that if one triggers, it cancels the other,
// but having restoreOpacity run more than once is a no-op for right now.
// Not a big deal if these don't get cleaned up.
// Have the form automatically reappear no matter what, so that if something
// does break, the user isn't left out to dry
window.setTimeout(restoreOpacity, 5_000);
/** @type {HTMLFormElement | null} */
const form = document.querySelector("web-client-form > form");
form?.addEventListener(
"submit",
() => {
// Not restoring opacity right away just to give the HTML canvas a little
// bit of time to get spun up and cover up the main form
window.setTimeout(restoreOpacity, 1_000);
},
{ once: true },
);
}
// Always safe to call these immediately because even if the Angular app isn't
// loaded by the time the function gets called, the CSS will always be globally
// available for when Angular is finally ready
setupAlwaysOnStyles();
hideFormForInitialSubmission();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", setupFormDetection);
} else {
setupFormDetection();
}

72
windows-rdp/main.test.ts Normal file
View File

@@ -0,0 +1,72 @@
import { describe, expect, it, test } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
type TestVariables = Readonly<{
agent_id: string;
resource_id: string;
admin_username?: string;
admin_password?: string;
}>;
describe("Web RDP", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
it("Installs the Devolutions Gateway Angular app locally on the machine", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
throw new Error("Not implemented yet");
});
/**
* @todo Verify that the HTML file has been modified, and that the JS file is
* also part of the file system
*/
it("Patches the Devolutions Angular app's .html file to include an import for the custom JS file", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "foo",
resource_id: "bar",
});
throw new Error("Not implemented yet");
});
it("Injects Terraform's username and password into the JS patch file", async () => {
throw new Error("Not implemented yet");
// Test that things work with the default username/password
const defaultState = await runTerraformApply<TestVariables>(
import.meta.dir,
{
agent_id: "foo",
resource_id: "bar",
},
);
const output = await executeScriptInContainer(defaultState, "alpine");
// Test that custom usernames/passwords are also forwarded correctly
const customUsername = "crouton";
const customPassword = "VeryVeryVeryVeryVerySecurePassword97!";
const customizedState = await runTerraformApply<TestVariables>(
import.meta.dir,
{
agent_id: "foo",
resource_id: "bar",
admin_username: customUsername,
admin_password: customPassword,
},
);
});
});

173
windows-rdp/main.tf Normal file
View File

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