Compare commits

...

76 Commits

Author SHA1 Message Date
timquinlan
5f312ced5e Merge pull request #237 from coder/maintainergithub
changed maintainer_github to coder, added partner_github: nataindata
2024-04-25 11:37:48 -04:00
timquinlan
fd985bedac changed maintainer_github to coder, added partner_github: nataindata 2024-04-25 15:25:12 +00:00
timquinlan
b0c14be846 Merge pull request #236 from coder/tim-airflow
corrected path in README.md to point to modules/apache-airflow
2024-04-25 09:29:48 -04:00
timquinlan
18efe83b89 corrected path in README.md to point to modules/apache-airflow 2024-04-25 13:17:08 +00:00
Muhammad Atif Ali
33dbae6ea0 fix(jetbrains-gateway): fix icon and name of coder_app (#233) 2024-04-24 23:42:55 +03:00
timquinlan
f14e6838e4 Merge pull request #227 from nataindata/apache-airflow
Apache Airflow module
2024-04-24 12:44:48 -04:00
timquinlan
2a30982d1a Update run.sh added export and scheduler lines 2024-04-24 12:43:16 -04:00
Stephen Kirby
47e995f636 fmt 2024-04-23 20:17:21 +00:00
nataindata
56fdf096c1 Apache Airflow 2024-04-18 17:28:09 +00:00
github-actions[bot]
49df203bd6 chore: bump version to 1.0.12 in README.md files (#230)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-04-18 18:13:02 +03:00
Michael Brewer
8766c670e6 feat(git-clone): add support for tree github or gitlab clone url (#210) 2024-04-17 11:40:47 -08:00
Muhammad Atif Ali
43304e5d4e docs(jetbrains-gateway): add examples on how to use the latest version (#228) 2024-04-17 11:27:49 +03:00
Muhammad Atif Ali
d8f71e4571 feat(jetbrains-gateway): Allow fetching latest version dynamically (#226) 2024-04-17 11:05:04 +03:00
nataindata
d8102e62ec Apache Airflow module 2024-04-16 17:05:44 +00:00
Muhammad Atif Ali
ed16ba59a9 fix(dotfiles): fix typo and remove a less useful output (#225) 2024-04-15 20:31:32 +03:00
Michael Brewer
a8c659ad6f feat: add coder_parameter_order to all data.coder_parameter fields (#223) 2024-04-15 20:31:21 +03:00
Michael Brewer
c4df384f4b feat(code-server): add extension_dir variable (#205) 2024-04-14 17:14:47 +03:00
Michael Brewer
892174da7c feat(git-config): allow data.coder_workspace.me.owner_email to be blank (#222) 2024-04-14 17:10:33 +03:00
djarbz
24e50e2bbb Dotfiles template default repo (#224)
Co-authored-by: Muhammad Atif Ali <me@matifali.dev>
2024-04-14 17:06:56 +03:00
github-actions[bot]
dfe69f25ce chore: bump version to 1.0.11 in README.md files (#221)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-04-11 02:39:55 +03:00
Michael Brewer
e8f6578ece feat(jetbrains-gateway): bump version to 2024.1 (#220) 2024-04-11 02:36:25 +03: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
Muhammad Atif Ali
ac64af6f02 Update Hashicorp vault modules (#140)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-14 01:43:33 +03:00
Muhammad Atif Ali
b299f98161 ci: automate version bumps in module README.md files (#139)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-13 15:23:44 +03:00
Muhammad Atif Ali
7e897a51e6 chore(vault-github): Add partner github and tests (#142) 2024-02-13 12:18:23 +03:00
Muhammad Atif Ali
ac54966f5e feat!(git-config): use full name for git configuration (#141) 2024-02-12 17:18:13 +03:00
Andrew Svoboda
aef9b3b116 Add build numbers and versions to jetbrains gateway module (#150) 2024-02-12 17:16:31 +03:00
Phorcys
a5c4d00a01 fix(git-commit-signing): fix SSH key permissions (#152) 2024-02-10 00:20:05 +03:00
Muhammad Atif Ali
3227a47044 fix(jetbrains-gateway): fix readme to include agent_name (#151) 2024-02-09 21:19:29 +03:00
Florian Gareis
cf1807dd5c Allow custom display name and slug for VS Code Web (#146) 2024-02-09 21:18:20 +03:00
Florian Gareis
4c993d342d Fix code-server docu (#147) 2024-02-09 21:17:32 +03:00
Muhammad Atif Ali
5a7e3f6ca4 Add Hashicorp Vault Secrets Integration module (#144) 2024-02-09 21:16:41 +03:00
Muhammad Atif Ali
acab6437bc chore: bump version to 1.0.2 and add script to update them automatically. (#128)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-01-30 17:51:31 +03:00
Muhammad Atif Ali
f16d7ca3f5 docs(jfrog-oauth): fix documentation link 2024-01-30 12:41:09 +03:00
Mathias Fredriksson
a9a58bff32 chore: lint for tf/hcl blocks (#135)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-01-28 00:48:49 +03:00
Muhammad Atif Ali
6b842004e6 ci: check for typos (#131) 2024-01-27 16:50:47 +03:00
Mathias Fredriksson
376c0cae31 chore: add prettier terraform formatting in markdown files (#134) 2024-01-27 15:02:40 +02:00
Muhammad Atif Ali
7d31865c94 feat!(git-clone): change path input to base_dir and return repo_dir as output (#132) 2024-01-26 16:13:03 +03:00
Muhammad Atif Ali
d3fc2d2212 docs(jfrog-oauth): improve docs (#129)
* docs(jfrog-oauth): improve docs

Adds additional step and screenshot to show creating an OAuth app in JFrog platform

* Update README.md

* Add files via upload

* fmt

* move JFrog Artifactory integration setup instructions

* Update JFrog token documentation
2024-01-26 09:20:21 +03:00
97 changed files with 2840 additions and 631 deletions

View File

@@ -34,5 +34,7 @@ jobs:
run: bun install run: bun install
- name: Format - name: Format
run: bun fmt:ci run: bun fmt:ci
- name: typos-action
uses: crate-ci/typos@v1.17.2
- name: Lint - name: Lint
run: bun lint run: bun lint

42
.github/workflows/update-readme.yaml vendored Normal file
View File

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

19
.icons/airflow.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

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

BIN
.images/airflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
To create a new module, clone this repository and run: To create a new module, clone this repository and run:
```shell ```shell
./new.sh MOUDLE_NAME ./new.sh MODULE_NAME
``` ```
## Testing a Module ## Testing a Module
@@ -19,7 +19,7 @@ $ bun test -t '<module>'
You can test a module locally by updating the source as follows You can test a module locally by updating the source as follows
```hcl ```tf
module "example" { module "example" {
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>" source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
} }

View File

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

71
apache-airflow/README.md Normal file
View File

@@ -0,0 +1,71 @@
---
display_name: airflow
description: A module that adds Apache Airflow in your Coder template
icon: ../.icons/airflow.svg
maintainer_github: coder
partner_github: nataindata
verified: true
tags: [airflow, idea, web, helper]
---
# airflow
A module that adds Apache Airflow in your Coder template.
```tf
module "airflow" {
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
}
```
![Airflow](../.images/airflow.png)
## Examples
### Example 1
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```tf
module "airflow" {
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
]
}
```
Enter the `<author>.<name>` into the extensions array and code-server will automatically install on start.
### Example 2
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
```tf
module "airflow" {
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
}
```
### Example 3
Run code-server in the background, don't fetch it from GitHub:
```tf
module "airflow" {
source = "registry.coder.com/modules/apache-airflow/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
offline = true
}
```

65
apache-airflow/main.tf Normal file
View File

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

19
apache-airflow/run.sh Normal file
View File

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

View File

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

View File

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

View File

@@ -51,11 +51,25 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# This is a static list because the regions don't change _that_ # This is a static list because the regions don't change _that_
# frequently and including the `aws_regions` data source requires # frequently and including the `aws_regions` data source requires
# the provider, which requires a region. # the provider, which requires a region.
regions = { 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" = { "ap-northeast-1" = {
name = "Asia Pacific (Tokyo)" name = "Asia Pacific (Tokyo)"
icon = "/emojis/1f1ef-1f1f5.png" icon = "/emojis/1f1ef-1f1f5.png"
@@ -72,6 +86,10 @@ locals {
name = "Asia Pacific (Mumbai)" name = "Asia Pacific (Mumbai)"
icon = "/emojis/1f1ee-1f1f3.png" icon = "/emojis/1f1ee-1f1f3.png"
} }
"ap-south-2" = {
name = "Asia Pacific (Hyderabad)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-southeast-1" = { "ap-southeast-1" = {
name = "Asia Pacific (Singapore)" name = "Asia Pacific (Singapore)"
icon = "/emojis/1f1f8-1f1ec.png" icon = "/emojis/1f1f8-1f1ec.png"
@@ -80,18 +98,42 @@ locals {
name = "Asia Pacific (Sydney)" name = "Asia Pacific (Sydney)"
icon = "/emojis/1f1e6-1f1fa.png" 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" = { "ca-central-1" = {
name = "Canada (Central)" name = "Canada (Central)"
icon = "/emojis/1f1e8-1f1e6.png" icon = "/emojis/1f1e8-1f1e6.png"
} }
"ca-west-1" = {
name = "Canada West (Calgary)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"eu-central-1" = { "eu-central-1" = {
name = "EU (Frankfurt)" name = "EU (Frankfurt)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
} }
"eu-central-2" = {
name = "Europe (Zurich)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-north-1" = { "eu-north-1" = {
name = "EU (Stockholm)" name = "EU (Stockholm)"
icon = "/emojis/1f1ea-1f1fa.png" 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" = { "eu-west-1" = {
name = "EU (Ireland)" name = "EU (Ireland)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
@@ -104,6 +146,14 @@ locals {
name = "EU (Paris)" name = "EU (Paris)"
icon = "/emojis/1f1ea-1f1fa.png" 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" = { "sa-east-1" = {
name = "South America (São Paulo)" name = "South America (São Paulo)"
icon = "/emojis/1f1e7-1f1f7.png" icon = "/emojis/1f1e7-1f1f7.png"
@@ -132,6 +182,7 @@ data "coder_parameter" "region" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
dynamic "option" { dynamic "option" {
for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) }

View File

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

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

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

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "../test"; import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("code-server", async () => { describe("code-server", async () => {
await runTerraformInit(import.meta.dir); await runTerraformInit(import.meta.dir);
@@ -8,5 +12,27 @@ describe("code-server", async () => {
agent_id: "foo", 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 // More tests depend on shebang refactors
}); });

View File

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

View File

@@ -4,6 +4,40 @@ EXTENSIONS=("${EXTENSIONS}")
BOLD='\033[0;1m' BOLD='\033[0;1m'
CODE='\033[36;40;1m' CODE='\033[36;40;1m'
RESET='\033[0m' RESET='\033[0m'
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Set extension directory
EXTENSION_ARG=""
if [ -n "${EXTENSIONS_DIR}" ]; then
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
fi
function run_code_server() {
echo "👷 Running code-server in the background..."
echo "Check logs at ${LOG_PATH}!"
$CODE_SERVER "$EXTENSION_ARG" --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" printf "$${BOLD}Installing code-server!\n"
@@ -22,8 +56,6 @@ if [ $? -ne 0 ]; then
fi fi
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n" printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Install each extension... # Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
for extension in "$${EXTENSIONLIST[@]}"; do for extension in "$${EXTENSIONLIST[@]}"; do
@@ -31,20 +63,11 @@ for extension in "$${EXTENSIONLIST[@]}"; do
continue continue
fi fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($CODE_SERVER --install-extension "$extension") output=$($CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension")
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to install extension: $extension: $output" echo "Failed to install extension: $extension: $output"
exit 1 exit 1
fi fi
done done
# Check if the settings file exists... run_code_server
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 &

View File

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

View File

@@ -11,10 +11,23 @@ tags: [helper]
Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io)! Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command. Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io)! Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
```hcl ```tf
module "dotfiles" { module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder" source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
## Setting a default dotfiles repository
You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable:
```tf
module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
```

View File

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

View File

@@ -14,11 +14,24 @@ variable "agent_id" {
description = "The ID of a Coder agent." description = "The ID of a Coder agent."
} }
variable "default_dotfiles_uri" {
type = string
description = "The default dotfiles URI if the workspace user does not provide one."
default = ""
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_parameter" "dotfiles_uri" { data "coder_parameter" "dotfiles_uri" {
type = "string" type = "string"
name = "dotfiles_uri" name = "dotfiles_uri"
display_name = "Dotfiles URL (optional)" display_name = "Dotfiles URL (optional)"
default = "" order = var.coder_parameter_order
default = var.default_dotfiles_uri
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace" description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
mutable = true mutable = true
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"

View File

@@ -10,20 +10,20 @@ tags: [helper, parameter, instances, exoscale]
# exoscale-instance-type # exoscale-instance-type
A parameter with all Exoscale instance types. This allows developers to select A parameter with all Exoscale instance types. This allows developers to select
their desired virtuell machine for the workspace. their desired virtual machine for the workspace.
Customize the preselected parameter value: Customize the preselected parameter value:
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "standard.medium" default = "standard.medium"
} }
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {
@@ -42,14 +42,16 @@ resource "coder_metadata" "workspace_info" {
Change the display name a type using the corresponding maps: Change the display name a type using the corresponding maps:
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "standard.medium" default = "standard.medium"
custom_names = { custom_names = {
"standard.medium" : "Mittlere Instanz" # German translation "standard.medium" : "Mittlere Instanz" # German translation
} }
custom_descriptions = { custom_descriptions = {
"standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation "standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation
} }
@@ -57,7 +59,7 @@ module "exoscale-instance-type" {
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {
@@ -70,14 +72,14 @@ resource "coder_metadata" "workspace_info" {
![Exoscale instance types Custom](../.images/exoscale-instance-custom.png) ![Exoscale instance types Custom](../.images/exoscale-instance-custom.png)
### Use category and exlude type ### Use category and exclude type
Show only gpu1 types Show only gpu1 types
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "gpu.large" default = "gpu.large"
type_category = ["gpu"] type_category = ["gpu"]
exclude = [ exclude = [
@@ -94,7 +96,7 @@ module "exoscale-instance-type" {
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {

View File

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

View File

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

View File

@@ -14,10 +14,10 @@ the zone closest to them.
Customize the preselected parameter value: Customize the preselected parameter value:
```hcl ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.0" version = "1.0.12"
default = "ch-dk-2" default = "ch-dk-2"
} }
@@ -29,7 +29,7 @@ data "exoscale_compute_template" "my_template" {
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value zone = module.exoscale-zone.value
.... # ...
} }
``` ```
@@ -41,14 +41,16 @@ resource "exoscale_compute_instance" "instance" {
Change the display name and icon for a zone using the corresponding maps: Change the display name and icon for a zone using the corresponding maps:
```hcl ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.0" version = "1.0.12"
default = "at-vie-1" default = "at-vie-1"
custom_names = { custom_names = {
"at-vie-1" : "Home Vienna" "at-vie-1" : "Home Vienna"
} }
custom_icons = { custom_icons = {
"at-vie-1" : "/emojis/1f3e0.png" "at-vie-1" : "/emojis/1f3e0.png"
} }
@@ -61,7 +63,7 @@ data "exoscale_compute_template" "my_template" {
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value zone = module.exoscale-zone.value
.... # ...
} }
``` ```
@@ -71,10 +73,10 @@ resource "exoscale_compute_instance" "instance" {
Hide the Switzerland zones Geneva and Zurich Hide the Switzerland zones Geneva and Zurich
```hcl ```tf
module "exoscale-zone" { module "exoscale-zone" {
source = "registry.coder.com/modules/exoscale-zone/coder" source = "registry.coder.com/modules/exoscale-zone/coder"
version = "1.0.0" version = "1.0.12"
exclude = ["ch-gva-2", "ch-dk-2"] exclude = ["ch-gva-2", "ch-dk-2"]
} }
@@ -85,7 +87,7 @@ data "exoscale_compute_template" "my_template" {
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
zone = module.exoscale-zone.value zone = module.exoscale-zone.value
.... # ...
} }
``` ```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ printf "$${BOLD}Installing filebrowser \n\n"
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
printf "🥳 Installation comlete! \n\n" printf "🥳 Installation complete! \n\n"
printf "👷 Starting filebrowser in background... \n\n" printf "👷 Starting filebrowser in background... \n\n"

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,35 +9,147 @@ tags: [git, helper]
# Git Clone # Git Clone
This module allows you to automatically clone a repository by URL and skip if it exists in the path provided. This module allows you to automatically clone a repository by URL and skip if it exists in the base directory provided.
```hcl ```tf
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
url = "https://github.com/coder/coder" url = "https://github.com/coder/coder"
} }
``` ```
To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-providers), add the provider by ID to your template:
```hcl
data "coder_git_auth" "github" {
id = "github"
}
```
## Examples ## Examples
### Custom Path ### Custom Path
```hcl ```tf
module "git-clone" { module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder" source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
url = "https://github.com/coder/coder" url = "https://github.com/coder/coder"
path = "~/projects/coder/coder" base_dir = "~/projects/coder"
}
```
### Git Authentication
To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-providers), add the provider by ID to your template:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
id = "github"
}
```
## GitHub clone with branch name
To GitHub clone with a specific branch like `feat/example`
```tf
# Prompt the user for the git repo URL
data "coder_parameter" "git_repo" {
name = "git_repo"
display_name = "Git repository"
default = "https://github.com/coder/coder/tree/feat/example"
}
# Clone the repository for branch `feat/example`
module "git_clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
# Create a code-server instance for the cloned repository
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone.folder_name}"
}
# Create a Coder app for the website
resource "coder_app" "website" {
agent_id = coder_agent.example.id
order = 2
slug = "website"
external = true
display_name = module.git_clone.folder_name
url = module.git_clone.web_url
icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg"
count = module.git_clone.web_url != "" ? 1 : 0
}
```
Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
## GitLab clone with branch name
To GitLab clone with a specific branch like `feat/example`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
```
Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
## Git clone with branch_name set
Alternatively, you can set the `branch_name` attribute to clone a specific branch.
For example, to clone the `feat/example` branch:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
} }
``` ```

View File

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

View File

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

View File

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

View File

@@ -16,10 +16,10 @@ Please observe that using the SSH key that's part of your Coder account for comm
This module has a chance of conflicting with the user's dotfiles / the personalize module if one of those has configuration directives that overwrite this module's / each other's git configuration. This module has a chance of conflicting with the user's dotfiles / the personalize module if one of those has configuration directives that overwrite this module's / each other's git configuration.
```hcl ```tf
module "git-commit-signing" { module "git-commit-signing" {
source = "registry.coder.com/modules/git-commit-signing/coder" source = "registry.coder.com/modules/git-commit-signing/coder"
version = "1.0.0" version = "1.0.11"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -16,7 +16,7 @@ variable "agent_id" {
resource "coder_script" "git-commit-signing" { resource "coder_script" "git-commit-signing" {
display_name = "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") script = file("${path.module}/run.sh")
run_on_start = true run_on_start = true

View File

@@ -21,7 +21,8 @@ echo "Downloading SSH key"
ssh_key=$(curl --request GET \ ssh_key=$(curl --request GET \
--url "${CODER_AGENT_URL}api/v2/workspaceagents/me/gitsshkey" \ --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 jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
$ssh_key $ssh_key
@@ -31,8 +32,8 @@ jq --raw-output ".private_key" > ~/.ssh/git-commit-signing/coder << EOF
$ssh_key $ssh_key
EOF EOF
chmod -R 400 ~/.ssh/git-commit-signing/coder chmod -R 600 ~/.ssh/git-commit-signing/coder
chmod -R 400 ~/.ssh/git-commit-signing/coder.pub chmod -R 644 ~/.ssh/git-commit-signing/coder.pub
echo "Configuring git to use the SSH key" echo "Configuring git to use the SSH key"

View File

@@ -11,10 +11,10 @@ tags: [helper, git]
Runs a script that updates git credentials in the workspace to match the user's Coder credentials, optionally allowing to the developer to override the defaults. Runs a script that updates git credentials in the workspace to match the user's Coder credentials, optionally allowing to the developer to override the defaults.
```hcl ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -25,10 +25,10 @@ TODO: Add screenshot
### Allow users to override both username and email ### Allow users to override both username and email
```hcl ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
allow_email_change = true allow_email_change = true
} }
@@ -38,14 +38,12 @@ TODO: Add screenshot
## Disallowing users from overriding both username and email ## Disallowing users from overriding both username and email
```hcl ```tf
module "git-config" { module "git-config" {
source = "registry.coder.com/modules/git-config/coder" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
allow_username_change = false allow_username_change = false
allow_email_change = false allow_email_change = false
} }
``` ```
TODO: Add screenshot

View File

@@ -1,6 +1,5 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
executeScriptInContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@@ -13,31 +12,88 @@ describe("git-config", async () => {
agent_id: "foo", agent_id: "foo",
}); });
it("fails without git", async () => { it("can run apply allow_username_change and allow_email_change disabled", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
allow_username_change: "false",
allow_email_change: "false",
}); });
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(1); const resources = state.resources;
expect(output.stdout).toEqual([ expect(resources).toHaveLength(3);
"\u001B[0;1mChecking git-config!", expect(resources).toMatchObject([
"Git is not installed!", { type: "coder_workspace", name: "me" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_name" },
]); ]);
}); });
it("runs with git", async () => { it("can run apply allow_email_change enabled", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
allow_email_change: "true",
}); });
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0); const resources = state.resources;
expect(output.stdout).toEqual([ expect(resources).toHaveLength(5);
"\u001B[0;1mChecking git-config!", expect(resources).toMatchObject([
"git-config: No user.email found, setting to ", { type: "coder_parameter", name: "user_email" },
"git-config: No user.name found, setting to default", { type: "coder_parameter", name: "username" },
"", { type: "coder_workspace", name: "me" },
"\u001B[0;1mgit-config: using email: ", { type: "coder_env", name: "git_author_name" },
"\u001B[0;1mgit-config: using username: default", { type: "coder_env", name: "git_commmiter_name" },
]); ]);
}); });
it("can run apply allow_email_change enabled", async () => {
const state = await runTerraformApply(
import.meta.dir,
{
agent_id: "foo",
allow_username_change: "false",
allow_email_change: "false",
},
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@emai.com" },
);
const resources = state.resources;
expect(resources).toHaveLength(5);
expect(resources).toMatchObject([
{ type: "coder_workspace", name: "me" },
{ type: "coder_env", name: "git_author_email" },
{ type: "coder_env", name: "git_author_name" },
{ type: "coder_env", name: "git_commmiter_email" },
{ type: "coder_env", name: "git_commmiter_name" },
]);
});
it("set custom order for coder_parameter for both fields", async () => {
const order = 20;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_username_change: "true",
allow_email_change: "true",
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(5);
// user_email order is the same as the order
expect(state.resources[0].instances[0].attributes.order).toBe(order);
// username order is incremented by 1
// @ts-ignore: Object is possibly 'null'.
expect(state.resources[1].instances[0]?.attributes.order).toBe(order + 1);
});
it("set custom order for coder_parameter for just username", async () => {
const order = 30;
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
allow_email_change: "false",
allow_username_change: "true",
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(4);
// user_email was not created
// username order is incremented by 1
expect(state.resources[0].instances[0].attributes.order).toBe(order + 1);
});
}); });

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.13"
} }
} }
} }
@@ -26,6 +26,11 @@ variable "allow_email_change" {
default = false default = false
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
@@ -34,7 +39,8 @@ data "coder_parameter" "user_email" {
name = "user_email" name = "user_email"
type = "string" type = "string"
default = "" default = ""
description = "Git user.email to be used for commits. Leave empty to default to Coder username." order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null
description = "Git user.email to be used for commits. Leave empty to default to Coder user's email."
display_name = "Git config user.email" display_name = "Git config user.email"
mutable = true mutable = true
} }
@@ -44,18 +50,34 @@ data "coder_parameter" "username" {
name = "username" name = "username"
type = "string" type = "string"
default = "" default = ""
description = "Git user.name to be used for commits. Leave empty to default to Coder username." order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null
display_name = "Git config user.name" description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
display_name = "Full Name for Git config"
mutable = true mutable = true
} }
resource "coder_script" "git_config" { resource "coder_env" "git_author_name" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { name = "GIT_AUTHOR_NAME"
GIT_USERNAME = try(data.coder_parameter.username[0].value, "") == "" ? data.coder_workspace.me.owner : try(data.coder_parameter.username[0].value, "") value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
GIT_EMAIL = try(data.coder_parameter.user_email[0].value, "") == "" ? data.coder_workspace.me.owner_email : try(data.coder_parameter.user_email[0].value, "") }
})
display_name = "Git Config" resource "coder_env" "git_commmiter_name" {
icon = "/icon/git.svg" agent_id = var.agent_id
run_on_start = true name = "GIT_COMMITTER_NAME"
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
}
resource "coder_env" "git_author_email" {
agent_id = var.agent_id
name = "GIT_AUTHOR_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
}
resource "coder_env" "git_commmiter_email" {
agent_id = var.agent_id
name = "GIT_COMMITTER_EMAIL"
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
} }

View File

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

View File

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

73
hcp-vault-secrets/main.tf Normal file
View File

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

View File

@@ -11,14 +11,15 @@ tags: [ide, jetbrains, helper, parameter]
This module adds a JetBrains Gateway Button to open any workspace with a single click. This module adds a JetBrains Gateway Button to open any workspace with a single click.
```hcl ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS", "IU", "PY", "PS", "CL", "RM"] jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
default = "PY" default = "GO"
} }
``` ```
@@ -26,19 +27,51 @@ module "jetbrains_gateway" {
## Examples ## Examples
### Add GoLand and WebStorm with the default set to GoLand ### Add GoLand and WebStorm as options with the default set to GoLand
```hcl ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.0" version = "1.0.12"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"] jetbrains_ides = ["GO", "WS"]
default = "GO" default = "GO"
} }
``` ```
### Use the latest release version
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
latest = true
}
```
### Use the latest EAP version
```tf
module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
default = "GO"
latest = true
channel = "eap"
}
```
## Supported IDEs ## Supported IDEs
This module and JetBrains Gateway support the following JetBrains IDEs: This module and JetBrains Gateway support the following JetBrains IDEs:
@@ -50,3 +83,4 @@ This module and JetBrains Gateway support the following JetBrains IDEs:
- PhpStorm (`PS`) - PhpStorm (`PS`)
- CLion (`CL`) - CLion (`CL`)
- RubyMine (`RM`) - RubyMine (`RM`)
- Rider (`RD`)

View File

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

View File

@@ -4,7 +4,11 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.11" version = ">= 0.17"
}
http = {
source = "hashicorp/http"
version = ">= 3.0"
} }
} }
} }
@@ -22,6 +26,10 @@ variable "agent_name" {
variable "folder" { variable "folder" {
type = string type = string
description = "The directory to open in the IDE. e.g. /home/coder/project" description = "The directory to open in the IDE. e.g. /home/coder/project"
validation {
condition = can(regex("^(?:/[^/]+)+$", var.folder))
error_message = "The folder must be a full path and must not start with a ~."
}
} }
variable "default" { variable "default" {
@@ -30,17 +38,95 @@ variable "default" {
description = "Default IDE" description = "Default IDE"
} }
variable "jetbrains_ides" { variable "order" {
type = list(string) type = number
description = "The list of IDE product codes." 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 = ["IU", "PS", "WS", "PY", "CL", "GO", "RM"] 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 "latest" {
type = bool
description = "Whether to fetch the latest version of the IDE."
default = false
}
variable "channel" {
type = string
description = "JetBrains IDE release channel. Valid values are release and eap."
default = "release"
validation {
condition = can(regex("^(release|eap)$", var.channel))
error_message = "The channel must be either release or eap."
}
}
variable "jetbrains_ide_versions" {
type = map(object({
build_number = string
version = string
}))
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "241.14494.240"
version = "2024.1"
}
"PS" = {
build_number = "241.14494.237"
version = "2024.1"
}
"WS" = {
build_number = "241.14494.235"
version = "2024.1"
}
"PY" = {
build_number = "241.14494.241"
version = "2024.1"
}
"CL" = {
build_number = "241.14494.288"
version = "2024.1"
}
"GO" = {
build_number = "241.14494.238"
version = "2024.1"
}
"RM" = {
build_number = "241.14494.234"
version = "2024.1"
}
"RD" = {
build_number = "241.14494.307"
version = "2024.1"
}
}
validation { validation {
condition = ( condition = (
alltrue([ alltrue([
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM"], code) for code in keys(var.jetbrains_ide_versions) : 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 IU, PS, WS, PY, CL, GO, RM." 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 = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
validation {
condition = (
alltrue([
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(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
} }
# check if the list is empty # check if the list is empty
validation { validation {
@@ -54,61 +140,104 @@ variable "jetbrains_ides" {
} }
} }
data "http" "jetbrains_ide_versions" {
for_each = var.latest ? toset(var.jetbrains_ides) : toset([])
url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}"
}
locals { locals {
jetbrains_ides = { jetbrains_ides = {
"GO" = { "GO" = {
icon = "/icon/goland.svg", icon = "/icon/goland.svg",
name = "GoLand", name = "GoLand",
value = jsonencode(["GO", "232.10203.20", "https://download.jetbrains.com/go/goland-2023.2.4.tar.gz"]) 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"
version = var.jetbrains_ide_versions["GO"].version
}, },
"WS" = { "WS" = {
icon = "/icon/webstorm.svg", icon = "/icon/webstorm.svg",
name = "WebStorm", name = "WebStorm",
value = jsonencode(["WS", "232.10203.14", "https://download.jetbrains.com/webstorm/WebStorm-2023.2.4.tar.gz"]) 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"
version = var.jetbrains_ide_versions["WS"].version
}, },
"IU" = { "IU" = {
icon = "/icon/intellij.svg", icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Ultimate", name = "IntelliJ IDEA Ultimate",
value = jsonencode(["IU", "232.10203.10", "https://download.jetbrains.com/idea/ideaIU-2023.2.4.tar.gz"]) 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"
version = var.jetbrains_ide_versions["IU"].version
}, },
"PY" = { "PY" = {
icon = "/icon/pycharm.svg", icon = "/icon/pycharm.svg",
name = "PyCharm Professional", name = "PyCharm Professional",
value = jsonencode(["PY", "232.10203.26", "https://download.jetbrains.com/python/pycharm-professional-2023.2.4.tar.gz"]) 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"
version = var.jetbrains_ide_versions["PY"].version
}, },
"CL" = { "CL" = {
icon = "/icon/clion.svg", icon = "/icon/clion.svg",
name = "CLion", name = "CLion",
value = jsonencode(["CL", "232.9921.42", "https://download.jetbrains.com/cpp/CLion-2023.2.2.tar.gz"]) 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"
version = var.jetbrains_ide_versions["CL"].version
}, },
"PS" = { "PS" = {
icon = "/icon/phpstorm.svg", icon = "/icon/phpstorm.svg",
name = "PhpStorm", name = "PhpStorm",
value = jsonencode(["PS", "232.10072.32", "https://download.jetbrains.com/webide/PhpStorm-2023.2.3.tar.gz"]) 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"
version = var.jetbrains_ide_versions["PS"].version
}, },
"RM" = { "RM" = {
icon = "/icon/rubymine.svg", icon = "/icon/rubymine.svg",
name = "RubyMine", name = "RubyMine",
value = jsonencode(["RM", "232.10203.15", "https://download.jetbrains.com/ruby/RubyMine-2023.2.4.tar.gz"]) 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"
version = var.jetbrains_ide_versions["RM"].version
} }
"RD" = {
icon = "/icon/rider.svg",
name = "Rider",
identifier = "RD",
build_number = var.jetbrains_ide_versions["RD"].build_number,
download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
version = var.jetbrains_ide_versions["RD"].version
} }
} }
icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon
json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {}
key = var.latest ? keys(local.json_data)[0] : ""
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
identifier = data.coder_parameter.jetbrains_ide.value
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number
version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
}
data "coder_parameter" "jetbrains_ide" { data "coder_parameter" "jetbrains_ide" {
type = "list(string)" type = "string"
name = "jetbrains_ide" name = "jetbrains_ide"
display_name = "JetBrains IDE" display_name = "JetBrains IDE"
icon = "/icon/gateway.svg" icon = "/icon/gateway.svg"
mutable = true mutable = true
# 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 == "" ? var.jetbrains_ides[0] : var.default
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 order = var.coder_parameter_order
dynamic "option" { 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 { content {
icon = option.value.icon icon = local.jetbrains_ides[option.value].icon
name = option.value.name name = local.jetbrains_ides[option.value].name
value = option.value.value value = option.value
} }
} }
} }
@@ -117,10 +246,11 @@ data "coder_workspace" "me" {}
resource "coder_app" "gateway" { resource "coder_app" "gateway" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].name
slug = "gateway" slug = "gateway"
icon = data.coder_parameter.jetbrains_ide.option[index(data.coder_parameter.jetbrains_ide.option.*.value, data.coder_parameter.jetbrains_ide.value)].icon display_name = local.display_name
icon = local.icon
external = true external = true
order = var.order
url = join("", [ url = join("", [
"jetbrains-gateway://connect#type=coder&workspace=", "jetbrains-gateway://connect#type=coder&workspace=",
data.coder_workspace.me.name, data.coder_workspace.me.name,
@@ -133,14 +263,38 @@ resource "coder_app" "gateway" {
"&token=", "&token=",
"$SESSION_TOKEN", "$SESSION_TOKEN",
"&ide_product_code=", "&ide_product_code=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[0], data.coder_parameter.jetbrains_ide.value,
"&ide_build_number=", "&ide_build_number=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[1], local.build_number,
"&ide_download_link=", "&ide_download_link=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[2], local.download_link,
]) ])
} }
output "jetbrains_ides" { output "identifier" {
value = data.coder_parameter.jetbrains_ide.value value = local.identifier
}
output "display_name" {
value = local.display_name
}
output "icon" {
value = local.icon
}
output "download_link" {
value = local.download_link
}
output "build_number" {
value = local.build_number
}
output "version" {
value = local.version
}
output "url" {
value = coder_app.gateway.url
} }

View File

@@ -10,19 +10,18 @@ tags: [integration, jfrog]
# JFrog # JFrog
Install the JF CLI and authenticate package managers with Artifactory using OAuth configured via the Coder `external-auth` feature. Install the JF CLI and authenticate package managers with Artifactory using OAuth configured via the Coder [`external-auth`](https://coder.com/docs/v2/latest/admin/external-auth) feature.
<p align="center"> ![JFrog OAuth](../.images/jfrog-oauth.png)
<img src='../.images/jfrog-oauth.png' alt="JFrog OAuth" width='600'>
</p>
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://jfrog.example.com" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
package_managers = { package_managers = {
"npm" : "npm", "npm" : "npm",
"go" : "go", "go" : "go",
@@ -36,62 +35,20 @@ module "jfrog" {
## Prerequisites ## Prerequisites
Coder [`external-auth`](https://coder.com/docs/v2/latest/admin/external-auth) configured with Artifactory. This requires a [custom integration](https://jfrog.com/help/r/jfrog-installation-setup-documentation/enable-new-integrations) in Artifactory with **Callback URL** set to `https://<your-coder-url>/external-auth/jfrog/callback`. This module is usable by JFrog self-hosted (on-premises) Artifactory as it requires configuring a custom integration. This integration benefits from Coder's [external-auth](https://coder.com/docs/v2/latest/admin/external-auth) feature and allows each user to authenticate with Artifactory using an OAuth flow and issues user-scoped tokens to each user. For configuration instructions, see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-oauth) on the Coder documentation.
To set this up,
1. Modify your `values.yaml` for JFrog Artifactory to add,
```yaml
artifactory:
enabled: true
frontend:
extraEnvironmentVariables:
- name: JF_FRONTEND_FEATURETOGGLER_ACCESSINTEGRATION
value: "true"
access:
accessConfig:
integrations-enabled: true
integration-templates:
- id: "1"
name: "CODER"
redirect-uri: "https://CODER_URL/external-auth/jfrog/callback"
scope: "applied-permissions/user"
```
> Note
> Replace `CODER_URL` with your Coder deployment URL, e.g., <coder.example.com>
2. Add a new [external authetication](https://coder.com/docs/v2/latest/admin/external-auth) to Coder by setting these env variables,
```env
# JFrog Artifactory External Auth
CODER_EXTERNAL_AUTH_1_ID="jfrog"
CODER_EXTERNAL_AUTH_1_TYPE="jfrog"
CODER_EXTERNAL_AUTH_1_CLIENT_ID="YYYYYYYYYYYYYYY"
CODER_EXTERNAL_AUTH_1_CLIENT_SECRET="XXXXXXXXXXXXXXXXXXX"
CODER_EXTERNAL_AUTH_1_DISPLAY_NAME="JFrog Artifactory"
CODER_EXTERNAL_AUTH_1_DISPLAY_ICON="/icon/jfrog.svg"
CODER_EXTERNAL_AUTH_1_AUTH_URL="https://JFROG_URL/ui/authorization"
CODER_EXTERNAL_AUTH_1_TOKEN_URL="https://JFROG_URL/access/api/v1/integrations/YYYYYYYYYYYYYYY/token"
CODER_EXTERNAL_AUTH_1_SCOPES="applied-permissions/user"
```
> Note
> Replace `JFROG_URL` with your JFrog Artifactory base URL, e.g., <artifactory.example.com>
## Examples ## Examples
Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username. Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://jfrog.example.com" jfrog_url = "https://example.jfrog.io"
auth_method = "oauth"
username_field = "email" username_field = "email"
package_managers = { package_managers = {
"pypi" : "pypi" "pypi" : "pypi"
} }
@@ -112,12 +69,12 @@ pip install requests
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE. The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://jfrog.example.com" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
configure_code_server = true # Add JFrog extension configuration for code-server configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = { package_managers = {
@@ -132,14 +89,15 @@ module "jfrog" {
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). 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).
```hcl ```tf
provider "docker" { provider "docker" {
... # ...
registry_auth { registry_auth {
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY" address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username username = module.jfrog.username
password = module.jfrog.access_token password = module.jfrog.access_token
} }
} }
``` ```
> Here `REPO_KEY` is the name of docker repository in Artifactory.

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" { variable "username_field" {
type = string type = string
description = "The field to use for the artifactory username. i.e. Coder username or email." 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", { script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url, JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email, ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token, ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,

View File

@@ -15,9 +15,9 @@ fi
# flows. # flows.
export CI=true export CI=true
# Authenticate JFrog CLI with Artifactory. # 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. # 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. # Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then if [ -z "${REPOSITORY_NPM}" ]; then

View File

@@ -12,10 +12,10 @@ tags: [integration, jfrog]
Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider. Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
@@ -27,14 +27,7 @@ module "jfrog" {
} }
``` ```
Get a JFrog access token from your Artifactory instance. The token must be an [admin token](https://registry.terraform.io/providers/jfrog/artifactory/latest/docs#access-token). It is recommended to store the token in a secret terraform variable. For detailed instructions, please see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-token) on the Coder documentation.
```hcl
variable "artifactory_access_token" {
type = string
sensitive = true
}
```
> Note > Note
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself. > This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
@@ -45,10 +38,10 @@ variable "artifactory_access_token" {
### Configure npm, go, and pypi to use Artifactory local repositories ### Configure npm, go, and pypi to use Artifactory local repositories
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io" jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token artifactory_access_token = var.artifactory_access_token # An admin access token
@@ -78,10 +71,10 @@ pip install requests
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE. The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.10"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
@@ -94,14 +87,34 @@ 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 ### 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). 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).
```hcl ```tf
provider "docker" { provider "docker" {
... # ...
registry_auth { registry_auth {
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY" address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username username = module.jfrog.username
@@ -109,3 +122,5 @@ provider "docker" {
} }
} }
``` ```
> Here `REPO_KEY` is the name of docker repository in Artifactory.

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" { variable "artifactory_access_token" {
type = string type = string
description = "The admin-level access token to use for JFrog." 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" { variable "check_license" {
type = bool type = bool
description = "Toggle for pre-flight checking of Artifactory license. Default to `true`." 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"] scopes = ["applied-permissions/user"]
refreshable = var.refreshable refreshable = var.refreshable
expires_in = var.expires_in expires_in = var.expires_in
description = var.token_description
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
@@ -112,6 +125,7 @@ resource "coder_script" "jfrog" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url, JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email, ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token, ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,

View File

@@ -15,9 +15,9 @@ fi
# flows. # flows.
export CI=true export CI=true
# Authenticate JFrog CLI with Artifactory. # 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. # 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. # Configure npm to use the Artifactory "npm" repository.
if [ -z "${REPOSITORY_NPM}" ]; then if [ -z "${REPOSITORY_NPM}" ]; then

View File

@@ -13,10 +13,10 @@ A module that adds Jupyter Notebook in your Coder template.
![Jupyter Notebook](../.images/jupyter-notebook.png) ![Jupyter Notebook](../.images/jupyter-notebook.png)
```hcl ```tf
module "jupyter-notebook" { module "jupyter-notebook" {
source = "registry.coder.com/modules/jupyter-notebook/coder" source = "registry.coder.com/modules/jupyter-notebook/coder"
version = "1.0.0" version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

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

View File

@@ -13,10 +13,10 @@ A module that adds JupyterLab in your Coder template.
![JupyterLab](../.images/jupyterlab.png) ![JupyterLab](../.images/jupyterlab.png)
```hcl ```tf
module "jupyterlab" { module "jupyterlab" {
source = "registry.coder.com/modules/jupyterlab/coder" source = "registry.coder.com/modules/jupyterlab/coder"
version = "1.0.0" version = "1.0.8"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

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

43
lint.ts
View File

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

4
new.sh
View File

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

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": "^3.2.5",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.3.3"
}
},
"node_modules/@types/node": {
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"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.4",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.4.tgz",
"integrity": "sha512-E1kk0FNpxpkSSlCVXEa4HfyhSUEpKtCFrybPVyz1A4TEnBGy5bqqtSYkyjKTfKScdyZTBeFrTxJLiKGOIRWgwg==",
"dev": true,
"dependencies": {
"@types/node": "~20.11.3",
"@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.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": 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.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"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

@@ -2,17 +2,26 @@
"name": "modules", "name": "modules",
"scripts": { "scripts": {
"test": "bun test", "test": "bun test",
"fmt": "bun x prettier --plugin prettier-plugin-sh -w **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt **/*.tf .sample/main.tf", "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 --plugin prettier-plugin-sh --check **/*.sh .sample/run.sh new.sh **/*.ts **/*.md *.md && terraform fmt -check **/*.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": { "devDependencies": {
"bun-types": "^1.0.18", "bun-types": "^1.0.18",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^11.1.0", "marked": "^12.0.0",
"prettier-plugin-sh": "^0.13.1" "prettier": "^3.2.5",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.3.3" "typescript": "^5.3.3"
},
"prettier": {
"plugins": [
"prettier-plugin-sh",
"prettier-plugin-terraform-formatter"
]
} }
} }

View File

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

View File

@@ -54,10 +54,10 @@ slackme npm run long-build
3. Restart your Coder deployment. Any Template can now import the Slack Me module, and `slackme` will be available on the `$PATH`: 3. Restart your Coder deployment. Any Template can now import the Slack Me module, and `slackme` will be available on the `$PATH`:
```hcl ```tf
module "slackme" { module "slackme" {
source = "registry.coder.com/modules/slackme/coder" source = "registry.coder.com/modules/slackme/coder"
version = "1.0.0" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
} }
@@ -70,10 +70,10 @@ slackme npm run long-build
- `$COMMAND` is replaced with the command the user executed. - `$COMMAND` is replaced with the command the user executed.
- `$DURATION` is replaced with a human-readable duration the command took to execute. - `$DURATION` is replaced with a human-readable duration the command took to execute.
```hcl ```tf
module "slackme" { module "slackme" {
source = "registry.coder.com/modules/slackme/coder" source = "registry.coder.com/modules/slackme/coder"
version = "1.0.0" version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
slack_message = <<EOF slack_message = <<EOF

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

View File

@@ -171,9 +171,9 @@ export const testRequiredVariables = (
export const runTerraformApply = async ( export const runTerraformApply = async (
dir: string, dir: string,
vars: Record<string, string>, vars: Record<string, string>,
env: Record<string, string> = {},
): Promise<TerraformState> => { ): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key])); Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));
const proc = spawn( const proc = spawn(
[ [

29
update-version.sh Executable file
View File

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

View File

@@ -3,6 +3,7 @@ display_name: Hashicorp Vault Integration (GitHub)
description: Authenticates with Vault using GitHub description: Authenticates with Vault using GitHub
icon: ../.icons/vault.svg icon: ../.icons/vault.svg
maintainer_github: coder maintainer_github: coder
partner_github: hashicorp
verified: true verified: true
tags: [helper, integration, vault, github] tags: [helper, integration, vault, github]
--- ---
@@ -11,10 +12,10 @@ tags: [helper, integration, vault, github]
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using [external auth](https://coder.com/docs/v2/latest/admin/external-auth) for GitHub. This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using [external auth](https://coder.com/docs/v2/latest/admin/external-auth) for GitHub.
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
} }
@@ -23,13 +24,13 @@ module "vault" {
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault: Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
```shell ```shell
vault kv get -mount=secret my-secret vault kv get -namespace=coder -mount=secrets coder
``` ```
or using the Vault API: or using the Vault API:
```shell ```shell
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/secret/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) ![Vault login](../.images/vault-login.png)
@@ -42,10 +43,10 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
### Configure Vault integration with a different Coder GitHub external auth ID (i.e., not the default `github`) ### Configure Vault integration with a different Coder GitHub external auth ID (i.e., not the default `github`)
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id" coder_github_auth_id = "my-github-auth-id"
@@ -54,10 +55,10 @@ module "vault" {
### Configure Vault integration with a different Coder GitHub external auth ID and a different Vault GitHub auth path ### Configure Vault integration with a different Coder GitHub external auth ID and a different Vault GitHub auth path
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id" coder_github_auth_id = "my-github-auth-id"
@@ -67,10 +68,10 @@ module "vault" {
### Configure Vault integration and install a specific version of the Vault CLI ### Configure Vault integration and install a specific version of the Vault CLI
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0" vault_cli_version = "1.15.0"

11
vault-github/main.test.ts Normal file
View File

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

View File

@@ -49,7 +49,6 @@ resource "coder_script" "vault" {
display_name = "Vault (GitHub)" display_name = "Vault (GitHub)"
icon = "/icon/vault.svg" icon = "/icon/vault.svg"
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
VAULT_ADDR : var.vault_addr,
AUTH_PATH : var.vault_github_auth_path, AUTH_PATH : var.vault_github_auth_path,
GITHUB_EXTERNAL_AUTH_ID : data.coder_external_auth.github.id, GITHUB_EXTERNAL_AUTH_ID : data.coder_external_auth.github.id,
INSTALL_VERSION : var.vault_cli_version, INSTALL_VERSION : var.vault_cli_version,

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env bash
# Convert all templated variables to shell variables # Convert all templated variables to shell variables
INSTALL_VERSION=${INSTALL_VERSION} INSTALL_VERSION=${INSTALL_VERSION}
VAULT_ADDR=${VAULT_ADDR}
GITHUB_EXTERNAL_AUTH_ID=${GITHUB_EXTERNAL_AUTH_ID} GITHUB_EXTERNAL_AUTH_ID=${GITHUB_EXTERNAL_AUTH_ID}
AUTH_PATH=${AUTH_PATH} AUTH_PATH=${AUTH_PATH}
@@ -21,7 +20,7 @@ fetch() {
fi fi
} }
unzip() { unzip_safe() {
if command -v unzip > /dev/null 2>&1; then if command -v unzip > /dev/null 2>&1; then
command unzip "$@" command unzip "$@"
elif command -v busybox > /dev/null 2>&1; then elif command -v busybox > /dev/null 2>&1; then
@@ -32,15 +31,26 @@ unzip() {
fi fi
} }
install() {
# Get the architecture of the system
ARCH=$(uname -m)
if [ "$${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "$${ARCH}" = "aarch64" ]; then
ARCH="arm64"
else
printf "Unsupported architecture: $${ARCH}\n"
return 1
fi
# Fetch the latest version of Vault if INSTALL_VERSION is 'latest' # Fetch the latest version of Vault if INSTALL_VERSION is 'latest'
if [ "$${INSTALL_VERSION}" = "latest" ]; then 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}" printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
if [ -z "$${LATEST_VERSION}" ]; then if [ -z "$${LATEST_VERSION}" ]; then
printf "Failed to determine the latest Vault version.\n" printf "Failed to determine the latest Vault version.\n"
exit 1 return 1
fi fi
VERSION=$${LATEST_VERSION} INSTALL_VERSION=$${LATEST_VERSION}
fi fi
# Check if the vault CLI is installed and has the correct version # Check if the vault CLI is installed and has the correct version
@@ -58,31 +68,41 @@ if [ $${installation_needed} -eq 1 ]; then
if [ -z "$${CURRENT_VERSION}" ]; then if [ -z "$${CURRENT_VERSION}" ]; then
printf "Installing Vault CLI ...\n\n" printf "Installing Vault CLI ...\n\n"
else else
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VERSION}" printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}"
fi fi
fetch vault.zip "https://releases.hashicorp.com/vault/$${VERSION}/vault_$${VERSION}_linux_amd64.zip" fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip"
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
printf "Failed to download Vault.\n" printf "Failed to download Vault.\n"
exit 1 return 1
fi fi
unzip vault.zip if ! unzip_safe vault.zip; then
if [ $? -ne 0 ]; then
printf "Failed to unzip Vault.\n" printf "Failed to unzip Vault.\n"
exit 1 return 1
fi fi
rm vault.zip rm vault.zip
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
printf "Vault installed successfully!\n\n" printf "Vault installed successfully!\n\n"
else else
mkdir -p ~/.local/bin mkdir -p ~/.local/bin
mv vault ~/.local/bin/vault if ! mv vault ~/.local/bin/vault; then
if [ ! -f ~/.local/bin/vault ]; then
printf "Failed to move Vault to local bin.\n" printf "Failed to move Vault to local bin.\n"
exit 1 return 1
fi fi
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n" printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
fi fi
fi fi
return 0
}
TMP=$(mktemp -d)
if ! (
cd "$TMP"
install
); then
echo "Failed to install Vault CLI."
exit 1
fi
rm -rf "$TMP"
# Authenticate with Vault # Authenticate with Vault
printf "🔑 Authenticating with Vault ...\n\n" printf "🔑 Authenticating with Vault ...\n\n"
@@ -92,8 +112,6 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
export VAULT_ADDR="$${VAULT_ADDR}"
# Login to vault using the GitHub token # Login to vault using the GitHub token
printf "🔑 Logging in to Vault ...\n\n" printf "🔑 Logging in to Vault ...\n\n"
vault login -no-print -method=github -path=/$${AUTH_PATH} token="$${GITHUB_TOKEN}" vault login -no-print -method=github -path=/$${AUTH_PATH} token="$${GITHUB_TOKEN}"

83
vault-token/README.md Normal file
View File

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

12
vault-token/main.test.ts Normal file
View File

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

62
vault-token/main.tf Normal file
View File

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

103
vault-token/run.sh Normal file
View File

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

View File

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

View File

@@ -20,5 +20,18 @@ describe("vscode-desktop", async () => {
expect(state.outputs.vscode_url.value).toBe( expect(state.outputs.vscode_url.value).toBe(
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN", "vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
); );
const resources: any = state.resources;
expect(resources[1].instances[0].attributes.order).toBeNull();
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});
const resources: any = state.resources;
expect(resources[1].instances[0].attributes.order).toBe(22);
}); });
}); });

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.17"
} }
} }
} }
@@ -20,6 +20,12 @@ variable "folder" {
default = "" default = ""
} }
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
resource "coder_app" "vscode" { resource "coder_app" "vscode" {
@@ -28,6 +34,7 @@ resource "coder_app" "vscode" {
icon = "/icon/code.svg" icon = "/icon/code.svg"
slug = "vscode" slug = "vscode"
display_name = "VS Code Desktop" display_name = "VS Code Desktop"
order = var.order
url = var.folder != "" ? join("", [ url = var.folder != "" ? join("", [
"vscode://coder.coder-remote/open?owner=", "vscode://coder.coder-remote/open?owner=",
data.coder_workspace.me.owner, data.coder_workspace.me.owner,

View File

@@ -9,12 +9,12 @@ tags: [helper, ide, vscode, web]
# VS Code Web # VS Code Web
Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace 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.
```hcl ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.0" version = "1.0.11"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
accept_license = true accept_license = true
} }
@@ -26,13 +26,42 @@ module "vscode-web" {
### Install VS Code Web to a custom folder ### Install VS Code Web to a custom folder
```hcl ```tf
module "vscode-web" { module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.0" version = "1.0.11"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_dir = "/home/coder/.vscode-web" install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder" folder = "/home/coder"
accept_license = true accept_license = true
} }
``` ```
### Install Extensions
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.11"
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.11"
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 { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.17"
} }
} }
} }
@@ -20,6 +20,18 @@ variable "port" {
default = 13338 default = 13338
} }
variable "display_name" {
type = string
description = "The display name for the VS Code Web application."
default = "VS Code Web"
}
variable "slug" {
type = string
description = "The slug for the VS Code Web application."
default = "vscode-web"
}
variable "folder" { variable "folder" {
type = string type = string
description = "The folder to open in vscode-web." description = "The folder to open in vscode-web."
@@ -41,15 +53,21 @@ variable "log_path" {
default = "/tmp/vscode-web.log" default = "/tmp/vscode-web.log"
} }
variable "install_dir" { variable "install_prefix" {
type = string type = string
description = "The directory to install VS Code CLI" description = "The prefix to install vscode-web to."
default = "/tmp/vscode-cli" default = "/tmp/vscode-web"
}
variable "extensions" {
type = list(string)
description = "A list of extensions to install."
default = []
} }
variable "accept_license" { variable "accept_license" {
type = bool type = bool
description = "Accept the VS Code license. https://code.visualstudio.com/license" description = "Accept the VS Code Server license. https://code.visualstudio.com/license/server"
default = false default = false
validation { validation {
condition = var.accept_license == true condition = var.accept_license == true
@@ -57,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" { resource "coder_script" "vscode-web" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "VS Code Web" display_name = "VS Code Web"
@@ -64,19 +104,24 @@ resource "coder_script" "vscode-web" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
PORT : var.port, PORT : var.port,
LOG_PATH : var.log_path, LOG_PATH : var.log_path,
INSTALL_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 run_on_start = true
} }
resource "coder_app" "vscode-web" { resource "coder_app" "vscode-web" {
agent_id = var.agent_id agent_id = var.agent_id
slug = "vscode-web" slug = var.slug
display_name = "VS Code Web" display_name = var.display_name
url = var.folder == "" ? "http://localhost:${var.port}" : "http://localhost:${var.port}?folder=${var.folder}" url = var.folder == "" ? "http://localhost:${var.port}" : "http://localhost:${var.port}?folder=${var.folder}"
icon = "/icon/code.svg" icon = "/icon/code.svg"
subdomain = true subdomain = true
share = var.share share = var.share
order = var.order
healthcheck { healthcheck {
url = "http://localhost:${var.port}/healthz" 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' BOLD='\033[0;1m'
EXTENSIONS=("${EXTENSIONS}")
# Create install directory if it doesn't exist # Create install prefix
mkdir -p ${INSTALL_DIR} 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 # Download and extract vscode-server
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) 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 if [ $? -ne 0 ]; then
echo "Failed to install vscode-cli: $output" echo "Failed to install Microsoft Visual Studio Code Server: $output"
exit 1 exit 1
fi 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}!" 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 &