Compare commits

...

86 Commits

Author SHA1 Message Date
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
Conor Bèhard Roberts
38a2d86376 feat: enable basename of url to be added to custom path (git-clone) (#124) 2024-01-23 13:55:30 +03:00
Muhammad Atif Ali
b968a2aa22 feat: add version to module docs (#122) 2024-01-23 13:50:55 +03:00
Muhammad Atif Ali
5b3edd9bbd fix(code-server): write settings to User (#123) 2024-01-18 23:22:57 +03:00
Muhammad Atif Ali
5b2f3bd599 chore: fix formatting for jfrog-oauth README (#119) 2024-01-11 00:17:20 +03:00
Muhammad Atif Ali
357bd41252 feat(jfrog): add JFrog vscode extension, CLI completion and docker support (#115) 2024-01-05 17:41:04 +03:00
Muhammad Atif Ali
382933aece chore(jfrog-oauth): update JFrog OAuth module README (#114) 2023-12-25 20:54:40 +03:00
Muhammad Atif Ali
f8faea1855 feat(vault-github): use coder_env to set VAULT_ADDR in workspace (#112) 2023-12-16 19:00:50 +03:00
Muhammad Atif Ali
1e3bd2b04b Add formatting check for shell scripts (#106) 2023-12-16 17:39:08 +03:00
Muhammad Atif Ali
1e7f91231c fix(jetbrains-gateway): fix tests (#111) 2023-12-08 04:33:53 +03:00
Muhammad Atif Ali
c6b1990225 fix(jetbrains-gateway): fix tests 2023-12-08 04:20:05 +03:00
Muhammad Atif Ali
3878e66700 fix: use agent_name in jetbrains-gateway (#110) 2023-12-08 04:10:19 +03:00
Muhammad Atif Ali
d48b68d374 chore: update screenshot for vault-github module (#109) 2023-12-08 03:12:22 +03:00
Muhammad Atif Ali
a954af73c5 chore(jetbrans-gateway): update JetBrains IDEs and remove community editions
Another try at #96. I cannot reproduce the error on deployment and dev.coder.com.

It also removes Community editions, `Rider`, and `DataGrip` as they are not supported for Remote Development.
2023-12-08 02:34:14 +03:00
Muhammad Atif Ali
90853d78d4 Add files via upload 2023-12-08 01:49:33 +03:00
Asher
9fc5eb9d29 Add missing URL to VS Code desktop module
Without this the plugin will only work if the user has happened to log
in before and that URL was previously saved.
2023-11-29 11:58:28 -09:00
Muhammad Atif Ali
6366097eea Update Coder external-auth link in README.md (#104) 2023-11-29 13:53:00 +03:00
Muhammad Atif Ali
c1800b7a85 Add Hashicorp Vault Integration (GitHub) (#105)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-11-29 13:23:48 +03:00
Muhammad Atif Ali
98bb94c5f0 feat: add JFrog access token output to module output (#101) 2023-11-15 18:15:00 +03:00
Muhammad Atif Ali
8e3f48ce5c fix(jfrog-token)!: add attributes to fine control the token behaviour (#100) 2023-11-15 16:03:43 +03:00
Muhammad Atif Ali
73ef0dc7d0 Add JFrog (OAuth) integration module (#97) 2023-11-15 00:12:57 +03:00
Spike Curtis
4e7f1e0ffd Revert "chore(jetbrans-gateway): update JetBrains IDEs (#96)" (#98)
This reverts commit b0187c69c1.
2023-11-14 10:05:15 +03:00
Muhammad Atif Ali
b0187c69c1 chore(jetbrans-gateway): update JetBrains IDEs (#96) 2023-11-14 01:18:35 +03:00
phorcys420
4dc9eae9c9 feat: add git-commit-signing module (#94)
* feat: add git-commit-signing module

* feat(git-commit-signing): check for git and jq

* fix(git-commit-signing): only use icon once

* fix(git-commit-signing): fix typo in README

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* bun fmt

* chore: clarify readme SSH key paragraph

* fix: add `curl` as dependency

* feat: download keys to ~/.ssh/git-commit-signing

* feat: add conflict disclaimer

---------

Co-authored-by: Muhammad Atif Ali <matifali@live.com>
Co-authored-by: Atif Ali <atif@coder.com>
2023-11-03 20:42:45 +03:00
Muhammad Atif Ali
e2f4fcba4a fix(git-clone): update the required provider version (#95) 2023-10-26 12:39:12 +03:00
Eric Paulsen
08162f5894 Merge pull request #91 from coder/share-var
feat: share variable
2023-10-16 11:30:29 -04:00
Muhammad Atif Ali
4f78c20201 fmt 2023-10-16 17:59:18 +03:00
Muhammad Atif Ali
52e4f3fb6f bun fmt 2023-10-16 17:54:24 +03:00
masterwendu
e090e79d4f Add Exoscale instance type Module (#88) 2023-10-16 17:37:36 +03:00
masterwendu
24bf54d1bb Add Exoscale zone Module (#87) 2023-10-16 17:36:58 +03:00
Eric Paulsen
11d7787cb0 feat: share variable 2023-10-15 21:58:03 -04:00
Stephen Kirby
b6ec1d85a7 Merge pull request #90 from coder/code-server-extensions
code-server: fix multiple-extension installation
2023-10-13 14:49:33 -05:00
Stephen Kirby
eaf6fae789 added readme block 2023-10-13 19:43:18 +00:00
Stephen Kirby
93965edc97 applied array changes 2023-10-13 19:39:30 +00:00
Stephen Kirby
b04683ca4c arr printing 2023-10-13 19:38:20 +00:00
Stephen Kirby
7b71f610e5 arr printing 2023-10-13 19:37:40 +00:00
Stephen Kirby
7d4723336e arr printing 2023-10-13 19:36:13 +00:00
Stephen Kirby
8918d8aef5 arr printing 2023-10-13 19:35:26 +00:00
Stephen Kirby
1745c534ed complete reversion 2023-10-13 19:32:24 +00:00
Stephen Kirby
f5b7df46f2 removed shell 2023-10-13 19:30:25 +00:00
Stephen Kirby
1a71239436 invalid character debug 2023-10-13 19:29:36 +00:00
Stephen Kirby
c47cf97fd9 naming fix 2023-10-13 19:28:01 +00:00
Stephen Kirby
54fc306a95 naming fix 2023-10-13 19:26:55 +00:00
Stephen Kirby
71d7ba80a5 naming fix 2023-10-13 19:26:29 +00:00
Stephen Kirby
7f4da980d1 naming fix 2023-10-13 19:25:45 +00:00
Stephen Kirby
7fa87d3074 naming fix 2023-10-13 19:25:14 +00:00
Stephen Kirby
03a4cc01be array IFS 2023-10-13 19:20:52 +00:00
Stephen Kirby
8fac468bb4 printing arr 2023-10-13 19:18:46 +00:00
Stephen Kirby
c85bf5db4f printing arr 2023-10-13 19:13:52 +00:00
Stephen Kirby
972b5b8282 debugging arr 2023-10-13 19:01:31 +00:00
Stephen Kirby
151cb234b0 testing array reading 2023-10-13 18:58:26 +00:00
100 changed files with 3356 additions and 749 deletions

View File

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

1
.icons/exoscale.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 427 B

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

2
.icons/vault.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 506 B

BIN
.images/exoscale-custom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
.images/exoscale-zones.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
.images/jfrog-oauth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
.images/vault-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -11,9 +11,10 @@ tags: [helper]
<!-- Describes what this module does --> <!-- Describes what this module does -->
```hcl ```tf
module "MODULE_NAME" { module "MODULE_NAME" {
source = "https://registry.coder.com/modules/MODULE_NAME" source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
} }
``` ```
@@ -25,13 +26,14 @@ 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 = "https://registry.coder.com/modules/MODULE_NAME" source = "registry.coder.com/modules/MODULE_NAME/coder"
agent_id = coder_agent.example.id version = "1.0.2"
extensions = [ agent_id = coder_agent.example.id
"dracula-theme.theme-dracula" extensions = [
] "dracula-theme.theme-dracula"
]
} }
``` ```
@@ -41,14 +43,15 @@ 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 = "https://registry.coder.com/modules/MODULE_NAME" source = "registry.coder.com/modules/MODULE_NAME/coder"
agent_id = coder_agent.example.id version = "1.0.2"
extensions = [ "dracula-theme.theme-dracula" ] agent_id = coder_agent.example.id
settings = { extensions = [ "dracula-theme.theme-dracula" ]
"workbench.colorTheme" = "Dracula" settings = {
} "workbench.colorTheme" = "Dracula"
}
} }
``` ```
@@ -56,10 +59,11 @@ 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 = "https://registry.coder.com/modules/MODULE_NAME" source = "registry.coder.com/modules/MODULE_NAME/coder"
agent_id = coder_agent.example.id version = "1.0.2"
offline = true agent_id = coder_agent.example.id
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

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

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,8 +19,10 @@ $ 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
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>" module "example" {
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
}
``` ```
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR. > **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.

View File

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

View File

@@ -14,14 +14,15 @@ the region closest to them.
Customize the preselected parameter value: Customize the preselected parameter value:
```hcl ```tf
module "aws-region" { module "aws-region" {
source = "https://registry.coder.com/modules/aws-region" source = "registry.coder.com/modules/aws-region/coder"
default = "us-east-1" version = "1.0.2"
default = "us-east-1"
} }
provider "aws" { provider "aws" {
region = module.aws_region.value region = module.aws_region.value
} }
``` ```
@@ -33,20 +34,23 @@ 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 = "https://registry.coder.com/modules/aws-region" source = "registry.coder.com/modules/aws-region/coder"
default = "ap-south-1" version = "1.0.2"
custom_names = { default = "ap-south-1"
"ap-south-1": "Awesome Mumbai!"
} custom_names = {
custom_icons = { "ap-south-1" : "Awesome Mumbai!"
"ap-south-1": "/emojis/1f33a.png" }
}
custom_icons = {
"ap-south-1" : "/emojis/1f33a.png"
}
} }
provider "aws" { provider "aws" {
region = module.aws_region.value region = module.aws_region.value
} }
``` ```
@@ -56,14 +60,15 @@ 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 = "https://registry.coder.com/modules/aws-region" source = "registry.coder.com/modules/aws-region/coder"
exclude = [ "ap-northeast-2", "ap-northeast-3" ] version = "1.0.2"
exclude = ["ap-northeast-2", "ap-northeast-3"]
} }
provider "aws" { provider "aws" {
region = module.aws_region.value region = module.aws_region.value
} }
``` ```

View File

@@ -11,14 +11,15 @@ 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 = "https://registry.coder.com/modules/azure-region" source = "registry.coder.com/modules/azure-region/coder"
default = "eastus" version = "1.0.2"
default = "eastus"
} }
resource "azurem_resource_group" "example" { resource "azurem_resource_group" "example" {
location = module.azure_region.value location = module.azure_region.value
} }
``` ```
@@ -30,19 +31,20 @@ 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 = "https://registry.coder.com/modules/azure-region" source = "registry.coder.com/modules/azure-region/coder"
custom_names = { version = "1.0.2"
"australia": "Go Australia!" custom_names = {
} "australia" : "Go Australia!"
custom_icons = { }
"australia": "/icons/smiley.svg" custom_icons = {
} "australia" : "/icons/smiley.svg"
}
} }
resource "azurerm_resource_group" "example" { resource "azurerm_resource_group" "example" {
location = module.azure_region.value location = module.azure_region.value
} }
``` ```
@@ -52,19 +54,20 @@ 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 = "https://registry.coder.com/modules/azure-region" source = "registry.coder.com/modules/azure-region/coder"
exclude = [ version = "1.0.2"
"australia", exclude = [
"australiacentral2", "australia",
"australiaeast", "australiacentral2",
"australiasoutheast" "australiaeast",
] "australiasoutheast"
]
} }
resource "azurerm_resource_group" "example" { resource "azurerm_resource_group" "example" {
location = module.azure_region.value location = module.azure_region.value
} }
``` ```

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,10 +11,11 @@ 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 = "https://registry.coder.com/modules/code-server" source = "registry.coder.com/modules/code-server/coder"
agent_id = coder_agent.example.id version = "1.0.5"
agent_id = coder_agent.example.id
} }
``` ```
@@ -24,9 +25,10 @@ module "code-server" {
### Pin Versions ### Pin Versions
```hcl ```tf
module "code-server" { module "code-server" {
source = "https://registry.coder.com/modules/code-server" source = "registry.coder.com/modules/code-server/coder"
version = "1.0.5"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
install_version = "4.8.3" install_version = "4.8.3"
} }
@@ -36,13 +38,14 @@ 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 = "https://registry.coder.com/modules/code-server" source = "registry.coder.com/modules/code-server/coder"
agent_id = coder_agent.example.id version = "1.0.5"
extensions = [ agent_id = coder_agent.example.id
"dracula-theme.theme-dracula" extensions = [
] "dracula-theme.theme-dracula"
]
} }
``` ```
@@ -52,14 +55,28 @@ 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 = "https://registry.coder.com/modules/code-server" source = "registry.coder.com/modules/code-server/coder"
agent_id = coder_agent.example.id version = "1.0.5"
extensions = [ "dracula-theme.theme-dracula" ] agent_id = coder_agent.example.id
settings = { extensions = ["dracula-theme.theme-dracula"]
"workbench.colorTheme" = "Dracula" settings = {
} "workbench.colorTheme" = "Dracula"
}
}
```
### Install multiple extensions
Just run code-server in the background, don't fetch it from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.5"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
} }
``` ```
@@ -67,10 +84,11 @@ 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 = "https://registry.coder.com/modules/code-server" source = "registry.coder.com/modules/code-server/coder"
agent_id = coder_agent.example.id version = "1.0.5"
offline = true agent_id = coder_agent.example.id
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"
} }
} }
} }
@@ -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."
@@ -62,6 +68,21 @@ variable "install_version" {
default = "" default = ""
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "code-server" { resource "coder_script" "code-server" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "code-server" display_name = "code-server"
@@ -69,6 +90,7 @@ 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,
@@ -80,12 +102,13 @@ resource "coder_script" "code-server" {
resource "coder_app" "code-server" { resource "coder_app" "code-server" {
agent_id = var.agent_id agent_id = var.agent_id
slug = "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 = "owner" share = var.share
order = var.order
healthcheck { healthcheck {
url = "http://localhost:${var.port}/healthz" url = "http://localhost:${var.port}/healthz"

View File

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

View File

@@ -11,10 +11,11 @@ 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 = "https://registry.coder.com/modules/coder-login" source = "registry.coder.com/modules/coder-login/coder"
agent_id = coder_agent.example.id version = "1.0.2"
agent_id = coder_agent.example.id
} }
``` ```

View File

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

View File

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

View File

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

View File

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

View File

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

98
exoscale-zone/README.md Normal file
View File

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

View File

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

110
exoscale-zone/main.tf Normal file
View File

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

View File

@@ -11,10 +11,11 @@ tags: [helper, filebrowser]
A file browser for your workspace. A file browser for your workspace.
```hcl ```tf
module "filebrowser" { module "filebrowser" {
source = "https://registry.coder.com/modules/filebrowser" source = "registry.coder.com/modules/filebrowser/coder"
agent_id = coder_agent.example.id version = "1.0.3"
agent_id = coder_agent.example.id
} }
``` ```
@@ -24,20 +25,22 @@ module "filebrowser" {
### Serve a specific directory ### Serve a specific directory
```hcl ```tf
module "filebrowser" { module "filebrowser" {
source = "https://registry.coder.com/modules/filebrowser" source = "registry.coder.com/modules/filebrowser/coder"
agent_id = coder_agent.example.id version = "1.0.3"
folder = "/home/coder/project" agent_id = coder_agent.example.id
folder = "/home/coder/project"
} }
``` ```
### Specify location of `filebrowser.db` ### Specify location of `filebrowser.db`
```hcl ```tf
module "filebrowser" { module "filebrowser" {
source = "https://registry.coder.com/modules/filebrowser" source = "registry.coder.com/modules/filebrowser/coder"
agent_id = coder_agent.example.id version = "1.0.3"
database_path = ".config/filebrowser.db" agent_id = coder_agent.example.id
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"
} }
} }
} }
@@ -43,6 +43,21 @@ variable "folder" {
default = "~" default = "~"
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "filebrowser" { resource "coder_script" "filebrowser" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "File Browser" display_name = "File Browser"
@@ -64,5 +79,6 @@ resource "coder_app" "filebrowser" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
subdomain = true subdomain = true
share = "owner" 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"
@@ -21,6 +21,6 @@ printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n" printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} >${LOG_PATH} 2>&1 & filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} > ${LOG_PATH} 2>&1 &
printf "📝 Logs at ${LOG_PATH} \n\n" printf "📝 Logs at ${LOG_PATH} \n\n"

View File

@@ -13,10 +13,11 @@ 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 = "https://registry.coder.com/modules/fly-region" source = "registry.coder.com/modules/fly-region/coder"
default = "atl" version = "1.0.2"
default = "atl"
} }
``` ```
@@ -28,11 +29,12 @@ 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 = "https://registry.coder.com/modules/fly-region" source = "registry.coder.com/modules/fly-region/coder"
default = "ams" version = "1.0.2"
regions = ["ams", "arn", "atl"] default = "ams"
regions = ["ams", "arn", "atl"]
} }
``` ```
@@ -42,16 +44,19 @@ 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 = "https://registry.coder.com/modules/fly-region" source = "registry.coder.com/modules/fly-region/coder"
default = "ams" version = "1.0.2"
custom_icons = { default = "ams"
"ams" = "/emojis/1f90e.png"
} custom_icons = {
custom_names = { "ams" = "/emojis/1f90e.png"
"ams" = "We love the Netherlands!" }
}
custom_names = {
"ams" = "We love the Netherlands!"
}
} }
``` ```

View File

@@ -11,14 +11,15 @@ 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 = "https://registry.coder.com/modules/gcp-region" source = "registry.coder.com/modules/gcp-region/coder"
regions = ["us", "europe"] version = "1.0.2"
regions = ["us", "europe"]
} }
resource "google_compute_instance" "example" { resource "google_compute_instance" "example" {
zone = module.gcp_region.value zone = module.gcp_region.value
} }
``` ```
@@ -30,44 +31,47 @@ 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 = "https://registry.coder.com/modules/gcp-region" source = "registry.coder.com/modules/gcp-region/coder"
default = ["us-west1-a"] version = "1.0.2"
regions = ["us-west1"] default = ["us-west1-a"]
gpu_only = false regions = ["us-west1"]
gpu_only = false
} }
resource "google_compute_instance" "example" { resource "google_compute_instance" "example" {
zone = module.gcp_region.value zone = module.gcp_region.value
} }
``` ```
### 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 = "https://registry.coder.com/modules/gcp-region" source = "registry.coder.com/modules/gcp-region/coder"
regions = ["europe-west"] version = "1.0.2"
single_zone_per_region = false regions = ["europe-west"]
single_zone_per_region = false
} }
resource "google_compute_instance" "example" { resource "google_compute_instance" "example" {
zone = module.gcp_region.value zone = module.gcp_region.value
} }
``` ```
### 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 = "https://registry.coder.com/modules/gcp-region" source = "registry.coder.com/modules/gcp-region/coder"
regions = ["us", "europe"] version = "1.0.2"
gpu_only = true regions = ["us", "europe"]
single_zone_per_region = true gpu_only = true
single_zone_per_region = true
} }
resource "google_compute_instance" "example" { resource "google_compute_instance" "example" {
zone = module.gcp_region.value zone = module.gcp_region.value
} }
``` ```

View File

@@ -9,21 +9,14 @@ 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 = "https://registry.coder.com/modules/git-clone" source = "registry.coder.com/modules/git-clone/coder"
agent_id = coder_agent.example.id version = "1.0.2"
url = "https://github.com/coder/coder" agent_id = coder_agent.example.id
} 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"
} }
``` ```
@@ -31,11 +24,29 @@ data "coder_git_auth" "github" {
### Custom Path ### Custom Path
```hcl ```tf
module "git-clone" { module "git-clone" {
source = "https://registry.coder.com/modules/git-clone" source = "registry.coder.com/modules/git-clone/coder"
agent_id = coder_agent.example.id version = "1.0.2"
url = "https://github.com/coder/coder" agent_id = coder_agent.example.id
path = "~/projects/coder/coder" url = "https://github.com/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.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
id = "github"
} }
``` ```

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.11" version = ">= 0.12"
} }
} }
} }
@@ -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,10 +25,19 @@ variable "agent_id" {
type = string type = string
} }
locals {
clone_path = var.base_dir != "" ? join("/", [var.base_dir, replace(basename(var.url), ".git", "")]) : join("/", ["~", replace(basename(var.url), ".git", "")])
}
output "repo_dir" {
value = local.clone_path
description = "Full path of cloned repo directory"
}
resource "coder_script" "git_clone" { resource "coder_script" "git_clone" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
CLONE_PATH : var.path != "" ? var.path : join("/", ["~", basename(var.url)]), CLONE_PATH = local.clone_path
REPO_URL : var.url, REPO_URL : var.url,
}) })
display_name = "Git Clone" display_name = "Git Clone"

View File

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

View File

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

View File

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

41
git-commit-signing/run.sh Executable file
View File

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

View File

@@ -11,9 +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 = "https://registry.coder.com/modules/git-config" source = "registry.coder.com/modules/git-config/coder"
version = "1.0.3"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -24,10 +25,11 @@ 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 = "https://registry.coder.com/modules/git-config" source = "registry.coder.com/modules/git-config/coder"
agent_id = coder_agent.example.id version = "1.0.3"
agent_id = coder_agent.example.id
allow_email_change = true allow_email_change = true
} }
``` ```
@@ -36,13 +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 = "https://registry.coder.com/modules/git-config" source = "registry.coder.com/modules/git-config/coder"
agent_id = coder_agent.example.id version = "1.0.3"
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,43 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "../test";
describe("git-config", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("fails without git", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(1);
expect(output.stdout).toEqual([
"\u001B[0;1mChecking git-config!",
"Git is not installed!",
]);
});
it("runs with git", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual([
"\u001B[0;1mChecking git-config!",
"git-config: No user.email found, setting to ",
"git-config: No user.name found, setting to default",
"",
"\u001B[0;1mgit-config: using email: ",
"\u001B[0;1mgit-config: using username: default",
]);
});
});

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.13"
} }
} }
} }
@@ -34,7 +34,7 @@ data "coder_parameter" "user_email" {
name = "user_email" name = "user_email"
type = "string" type = "string"
default = "" default = ""
description = "Git user.email to be used for commits. Leave empty to default to Coder username." 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 +44,31 @@ 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." description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
display_name = "Git config user.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)
}
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)
} }

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

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.11" version = ">= 0.17"
} }
} }
} }
@@ -16,12 +16,16 @@ variable "agent_id" {
variable "agent_name" { variable "agent_name" {
type = string type = string
description = "The name of a Coder agent." description = "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,16 +34,69 @@ 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 = null
}
variable "jetbrains_ide_versions" {
type = map(object({
build_number = string
version = string
}))
description = "The set of versions for each jetbrains IDE"
default = {
"IU" = {
build_number = "232.10203.10"
version = "2023.2.4"
}
"PS" = {
build_number = "232.10072.32"
version = "2023.2.3"
}
"WS" = {
build_number = "232.10203.14"
version = "2023.2.4"
}
"PY" = {
build_number = "232.10203.26"
version = "2023.2.4"
}
"CL" = {
build_number = "232.9921.42"
version = "2023.2.2"
}
"GO" = {
build_number = "232.10203.20"
version = "2023.2.4"
}
"RM" = {
build_number = "232.10203.15"
version = "2023.2.4"
}
}
validation { validation {
condition = ( condition = (
alltrue([ alltrue([
for code in var.jetbrains_ides : contains(["IU", "IC", "PS", "WS", "PY", "PC", "CL", "GO", "DB", "RD", "RM"], code) for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM"], code)
]) ])
) )
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are: IU, IC, PS, WS, PY, PC, CL, GO, DB, RD, RM." 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"])}."
}
}
variable "jetbrains_ides" {
type = list(string)
description = "The list of IDE product codes."
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM"]
validation {
condition = (
alltrue([
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM"], code)
])
)
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM"])}."
} }
# check if the list is empty # check if the list is empty
validation { validation {
@@ -56,78 +113,71 @@ variable "jetbrains_ides" {
locals { locals {
jetbrains_ides = { jetbrains_ides = {
"GO" = { "GO" = {
icon = "/icon/goland.svg", icon = "/icon/goland.svg",
name = "GoLand", name = "GoLand",
value = jsonencode(["GO", "232.9921.53", "https://download.jetbrains.com/go/goland-2023.2.2.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"
}, },
"WS" = { "WS" = {
icon = "/icon/webstorm.svg", icon = "/icon/webstorm.svg",
name = "WebStorm", name = "WebStorm",
value = jsonencode(["WS", "232.9921.42", "https://download.jetbrains.com/webstorm/WebStorm-2023.2.2.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"
}, },
"IU" = { "IU" = {
icon = "/icon/intellij.svg", icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Ultimate", name = "IntelliJ IDEA Ultimate",
value = jsonencode(["IU", "232.9921.47", "https://download.jetbrains.com/idea/ideaIU-2023.2.2.tar.gz"]) identifier = "IU",
}, build_number = var.jetbrains_ide_versions["IU"].build_number,
"IC" = { download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
icon = "/icon/intellij.svg",
name = "IntelliJ IDEA Community",
value = jsonencode(["IC", "232.9921.47", "https://download.jetbrains.com/idea/ideaIC-2023.2.2.tar.gz"])
}, },
"PY" = { "PY" = {
icon = "/icon/pycharm.svg", icon = "/icon/pycharm.svg",
name = "PyCharm Professional", name = "PyCharm Professional",
value = jsonencode(["PY", "232.9559.58", "https://download.jetbrains.com/python/pycharm-professional-2023.2.1.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"
}, },
"PC" = {
icon = "/icon/pycharm.svg",
name = "PyCharm Community",
value = jsonencode(["PC", "232.9559.58", "https://download.jetbrains.com/python/pycharm-community-2023.2.1.tar.gz"])
},
"RD" = {
icon = "/icon/rider.svg",
name = "Rider",
value = jsonencode(["RD", "232.9559.61", "https://download.jetbrains.com/rider/JetBrains.Rider-2023.2.1.tar.gz"])
}
"CL" = { "CL" = {
icon = "/icon/clion.svg", icon = "/icon/clion.svg",
name = "CLion", name = "CLion",
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,
"DB" = { download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
icon = "/icon/datagrip.svg",
name = "DataGrip",
value = jsonencode(["DB", "232.9559.28", "https://download.jetbrains.com/datagrip/datagrip-2023.2.1.tar.gz"])
}, },
"PS" = { "PS" = {
icon = "/icon/phpstorm.svg", icon = "/icon/phpstorm.svg",
name = "PhpStorm", name = "PhpStorm",
value = jsonencode(["PS", "232.9559.64", "https://download.jetbrains.com/webide/PhpStorm-2023.2.1.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"
}, },
"RM" = { "RM" = {
icon = "/icon/rubymine.svg", icon = "/icon/rubymine.svg",
name = "RubyMine", name = "RubyMine",
value = jsonencode(["RM", "232.9921.48", "https://download.jetbrains.com/ruby/RubyMine-2023.2.2.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"
} }
} }
} }
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
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 = lookup(local.jetbrains_ides, option.value).icon
name = option.value.name name = lookup(local.jetbrains_ides, option.value).name
value = option.value.value value = lookup(local.jetbrains_ides, option.value).identifier
} }
} }
} }
@@ -136,10 +186,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 = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).name, "JetBrains IDE")
icon = try(lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).icon, "/icon/gateway.svg")
external = true 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,
@@ -152,14 +203,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], local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].identifier,
"&ide_build_number=", "&ide_build_number=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[1], local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number,
"&ide_download_link=", "&ide_download_link=",
jsondecode(data.coder_parameter.jetbrains_ide.value)[2], local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
]) ])
} }
output "jetbrains_ides" { output "identifier" {
value = data.coder_parameter.jetbrains_ide.value value = data.coder_parameter.jetbrains_ide.value
} }
output "name" {
value = coder_app.gateway.display_name
}
output "icon" {
value = coder_app.gateway.icon
}
output "download_link" {
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).download_link
}
output "build_number" {
value = lookup(local.jetbrains_ides, data.coder_parameter.jetbrains_ide.value).build_number
}
output "version" {
value = var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
}
output "url" {
value = coder_app.gateway.url
}

103
jfrog-oauth/README.md Normal file
View File

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

19
jfrog-oauth/main.test.ts Normal file
View File

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

138
jfrog-oauth/main.tf Normal file
View File

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

122
jfrog-oauth/run.sh Normal file
View File

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

106
jfrog-token/README.md Normal file
View File

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

View File

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

171
jfrog-token/main.tf Normal file
View File

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

122
jfrog-token/run.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -13,9 +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 = "https://registry.coder.com/modules/jupyter-notebook" source = "registry.coder.com/modules/jupyter-notebook/coder"
agent_id = coder_agent.example.id version = "1.0.2"
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"
} }
} }
} }
@@ -27,6 +27,21 @@ variable "port" {
default = 19999 default = 19999
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "jupyter-notebook" { resource "coder_script" "jupyter-notebook" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "jupyter-notebook" display_name = "jupyter-notebook"
@@ -45,5 +60,6 @@ resource "coder_app" "jupyter-notebook" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "/icon/jupyter.svg" icon = "/icon/jupyter.svg"
subdomain = true subdomain = true
share = "owner" share = var.share
order = var.order
} }

View File

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

View File

@@ -13,9 +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 = "https://registry.coder.com/modules/jupyterlab" source = "registry.coder.com/modules/jupyterlab/coder"
agent_id = coder_agent.example.id version = "1.0.2"
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"
} }
} }
} }
@@ -27,6 +27,21 @@ variable "port" {
default = 19999 default = 19999
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "jupyterlab" { resource "coder_script" "jupyterlab" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "jupyterlab" display_name = "jupyterlab"
@@ -45,5 +60,6 @@ resource "coder_app" "jupyterlab" {
url = "http://localhost:${var.port}" url = "http://localhost:${var.port}"
icon = "/icon/jupyter.svg" icon = "/icon/jupyter.svg"
subdomain = true subdomain = true
share = "owner" share = var.share
order = var.order
} }

View File

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

49
lint.ts
View File

@@ -13,9 +13,39 @@ let badExit = false;
// error reports an error to the console and sets badExit to true // error reports an error to the console and sets badExit to true
// so that the process will exit with a non-zero exit code. // so that the process will exit with a non-zero exit code.
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) {

26
new.sh
View File

@@ -1,21 +1,21 @@
#!/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
# Check if module name is provided # Check if module name is provided
if [ -z "$MODULE_NAME" ]; then if [ -z "$MODULE_NAME" ]; then
echo "Usage: ./new.sh <module_name>" echo "Usage: ./new.sh <module_name>"
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"
exit 1 exit 1
fi fi
mkdir -p "${MODULE_NAME}" mkdir -p "${MODULE_NAME}"
@@ -27,13 +27,13 @@ cd "${MODULE_NAME}"
# Detect OS # Detect OS
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS # macOS
sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" main.tf sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" main.tf
sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" README.md sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" README.md
else else
# Linux # Linux
sed -i "s/MODULE_NAME/${MODULE_NAME}/g" main.tf sed -i "s/MODULE_NAME/${MODULE_NAME}/g" main.tf
sed -i "s/MODULE_NAME/${MODULE_NAME}/g" README.md sed -i "s/MODULE_NAME/${MODULE_NAME}/g" README.md
fi fi
# Make run.sh executable # Make run.sh executable

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.2"
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.2"
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.2"
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."
default = "$HOME/.nvm"
}
variable "node_versions" {
type = list(string)
description = "A list of Node.js versions to install."
default = ["node"]
}
variable "default_node_version" {
type = string
description = "The default Node.js version"
default = "node"
}
resource "coder_script" "nodejs" {
agent_id = var.agent_id
display_name = "Node.js:"
script = templatefile("${path.module}/run.sh", {
NVM_VERSION : var.nvm_version,
INSTALL_PREFIX : var.nvm_install_prefix,
NODE_VERSIONS : join(",", var.node_versions),
DEFAULT : var.default_node_version,
})
run_on_start = true
start_blocks_login = true
}

50
nodejs/run.sh Executable file
View File

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

View File

@@ -2,16 +2,25 @@
"name": "modules", "name": "modules",
"scripts": { "scripts": {
"test": "bun test", "test": "bun test",
"fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.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 --check **/*.ts **/*.md *.md && terraform fmt -check **/*.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.3", "bun-types": "^1.0.18",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"marked": "^9.0.3" "marked": "^12.0.0",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.0.0" "typescript": "^5.3.3"
},
"prettier": {
"plugins": [
"prettier-plugin-sh",
"prettier-plugin-terraform-formatter"
]
} }
} }

View File

@@ -11,9 +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 = "https://registry.coder.com/modules/personalize" source = "registry.coder.com/modules/personalize/coder"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

@@ -9,18 +9,18 @@ SCRIPT="$${SCRIPT/#\~/$${HOME}}"
# If the personalize script doesn't exist, educate # If the personalize script doesn't exist, educate
# the user how they can customize their environment! # the user how they can customize their environment!
if [ ! -f $SCRIPT ]; then if [ ! -f $SCRIPT ]; then
printf "$${BOLD}You don't have a personalize script!\n\n" printf "$${BOLD}You don't have a personalize script!\n\n"
printf "Run $${CODE}touch $${SCRIPT} && chmod +x $${SCRIPT}$${RESET} to create one.\n" printf "Run $${CODE}touch $${SCRIPT} && chmod +x $${SCRIPT}$${RESET} to create one.\n"
printf "It will run every time your workspace starts. Use it to install personal packages!\n\n" printf "It will run every time your workspace starts. Use it to install personal packages!\n\n"
exit 0 exit 0
fi fi
# Check if the personalize script is executable, if not, # Check if the personalize script is executable, if not,
# try to make it executable and educate the user if it fails. # try to make it executable and educate the user if it fails.
if [ ! -x $SCRIPT ]; then if [ ! -x $SCRIPT ]; then
echo "🔐 Your personalize script isn't executable!" echo "🔐 Your personalize script isn't executable!"
printf "Run $CODE\`chmod +x $SCRIPT\`$RESET to make it executable.\n" printf "Run $CODE\`chmod +x $SCRIPT\`$RESET to make it executable.\n"
exit 0 exit 0
fi fi
# Run the personalize script! # Run the personalize script!

View File

@@ -12,7 +12,7 @@ tags: [helper]
Add the `slackme` command to your workspace that DMs you on Slack when your command finishes running. Add the `slackme` command to your workspace that DMs you on Slack when your command finishes running.
```bash ```bash
$ slackme npm run long-build slackme npm run long-build
``` ```
## Setup ## Setup
@@ -54,10 +54,11 @@ $ 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 = "https://registry.coder.com/modules/slackme" source = "registry.coder.com/modules/slackme/coder"
agent_id = coder_agent.example.id version = "1.0.2"
agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
} }
``` ```
@@ -69,12 +70,13 @@ $ 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 = "https://registry.coder.com/modules/slackme" source = "registry.coder.com/modules/slackme/coder"
agent_id = coder_agent.example.id version = "1.0.2"
agent_id = coder_agent.example.id
auth_provider_id = "slack" auth_provider_id = "slack"
slack_message = <<EOF slack_message = <<EOF
👋 Hey there from Coder! $COMMAND took $DURATION to execute! 👋 Hey there from Coder! $COMMAND took $DURATION to execute!
EOF EOF
} }

View File

@@ -1,14 +1,15 @@
#!/usr/bin/env sh #!/usr/bin/env sh
PROVIDER_ID=${PROVIDER_ID} PROVIDER_ID=${PROVIDER_ID}
SLACK_MESSAGE=$(cat << "EOF" SLACK_MESSAGE=$(
cat << "EOF"
${SLACK_MESSAGE} ${SLACK_MESSAGE}
EOF EOF
) )
SLACK_URL=$${SLACK_URL:-https://slack.com} SLACK_URL=$${SLACK_URL:-https://slack.com}
usage() { usage() {
cat <<EOF cat << EOF
slackme — Send a Slack notification when a command finishes slackme — Send a Slack notification when a command finishes
Usage: slackme <command> Usage: slackme <command>
@@ -17,45 +18,45 @@ EOF
} }
pretty_duration() { pretty_duration() {
local duration_ms=$1 local duration_ms=$1
# If the duration is less than 1 second, display in milliseconds # If the duration is less than 1 second, display in milliseconds
if [ $duration_ms -lt 1000 ]; then if [ $duration_ms -lt 1000 ]; then
echo "$${duration_ms}ms" echo "$${duration_ms}ms"
return return
fi fi
# Convert the duration to seconds # Convert the duration to seconds
local duration_sec=$((duration_ms / 1000)) local duration_sec=$((duration_ms / 1000))
local remaining_ms=$((duration_ms % 1000)) local remaining_ms=$((duration_ms % 1000))
# If the duration is less than 1 minute, display in seconds (with ms) # If the duration is less than 1 minute, display in seconds (with ms)
if [ $duration_sec -lt 60 ]; then if [ $duration_sec -lt 60 ]; then
echo "$${duration_sec}.$${remaining_ms}s" echo "$${duration_sec}.$${remaining_ms}s"
return return
fi fi
# Convert the duration to minutes # Convert the duration to minutes
local duration_min=$((duration_sec / 60)) local duration_min=$((duration_sec / 60))
local remaining_sec=$((duration_sec % 60)) local remaining_sec=$((duration_sec % 60))
# If the duration is less than 1 hour, display in minutes and seconds # If the duration is less than 1 hour, display in minutes and seconds
if [ $duration_min -lt 60 ]; then if [ $duration_min -lt 60 ]; then
echo "$${duration_min}m $${remaining_sec}.$${remaining_ms}s" echo "$${duration_min}m $${remaining_sec}.$${remaining_ms}s"
return return
fi fi
# Convert the duration to hours # Convert the duration to hours
local duration_hr=$((duration_min / 60)) local duration_hr=$((duration_min / 60))
local remaining_min=$((duration_min % 60)) local remaining_min=$((duration_min % 60))
# Display in hours, minutes, and seconds # Display in hours, minutes, and seconds
echo "$${duration_hr}hr $${remaining_min}m $${remaining_sec}.$${remaining_ms}s" echo "$${duration_hr}hr $${remaining_min}m $${remaining_sec}.$${remaining_ms}s"
} }
if [ $# -eq 0 ]; then if [ $# -eq 0 ]; then
usage usage
exit 1 exit 1
fi fi
BOT_TOKEN=$(coder external-auth access-token $PROVIDER_ID) BOT_TOKEN=$(coder external-auth access-token $PROVIDER_ID)
@@ -74,7 +75,7 @@ START=$(date +%s%N)
# Run all arguments as a command # Run all arguments as a command
$@ $@
END=$(date +%s%N) END=$(date +%s%N)
DURATION_MS=$${DURATION_MS:-$(( (END - START) / 1000000 ))} DURATION_MS=$${DURATION_MS:-$(((END - START) / 1000000))}
PRETTY_DURATION=$(pretty_duration $DURATION_MS) PRETTY_DURATION=$(pretty_duration $DURATION_MS)
set -e set -e
@@ -83,5 +84,5 @@ SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$COMMAND|$COMMAND|g")
SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g") SLACK_MESSAGE=$(echo "$SLACK_MESSAGE" | sed "s|\\$DURATION|$PRETTY_DURATION|g")
curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \ curl --silent -o /dev/null --header "Authorization: Bearer $BOT_TOKEN" \
-G --data-urlencode "text=$${SLACK_MESSAGE}" \ -G --data-urlencode "text=$${SLACK_MESSAGE}" \
"$SLACK_URL/api/chat.postMessage?channel=$USER_ID&pretty=1" "$SLACK_URL/api/chat.postMessage?channel=$USER_ID&pretty=1"

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

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

79
vault-github/README.md Normal file
View File

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

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",
});
});

68
vault-github/main.tf Normal file
View File

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

119
vault-github/run.sh Normal file
View File

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

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,9 +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 = "https://registry.coder.com/modules/vscode-desktop" source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.2"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@@ -24,10 +25,11 @@ module "vscode" {
### Open in a specific directory ### Open in a specific directory
```hcl ```tf
module "vscode" { module "vscode" {
source = "https://registry.coder.com/modules/vscode-desktop" source = "registry.coder.com/modules/vscode-desktop/coder"
version = "1.0.2"
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,
@@ -35,6 +42,8 @@ resource "coder_app" "vscode" {
data.coder_workspace.me.name, data.coder_workspace.me.name,
"&folder=", "&folder=",
var.folder, var.folder,
"&url=",
data.coder_workspace.me.access_url,
"&token=$SESSION_TOKEN", "&token=$SESSION_TOKEN",
]) : join("", [ ]) : join("", [
"vscode://coder.coder-remote/open?owner=", "vscode://coder.coder-remote/open?owner=",

View File

@@ -9,11 +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 = "https://registry.coder.com/modules/vscode-web" source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.6"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
accept_license = true accept_license = true
} }
@@ -25,12 +26,25 @@ 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 = "https://registry.coder.com/modules/vscode-web" source = "registry.coder.com/modules/vscode-web/coder"
agent_id = coder_agent.example.id version = "1.0.6"
install_dir = "/home/coder/.vscode-web" agent_id = coder_agent.example.id
folder = "/home/coder" install_prefix = "/home/coder/.vscode-web"
accept_license = true folder = "/home/coder"
accept_license = true
}
```
### Install Extensions
```tf
module "vscode-web" {
source = "registry.coder.com/modules/vscode-web/coder"
version = "1.0.6"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
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,27 +20,54 @@ variable "port" {
default = 13338 default = 13338
} }
variable "display_name" {
type = string
description = "The display name for the VS Code Web application."
default = "VS Code Web"
}
variable "slug" {
type = string
description = "The slug for the VS Code Web application."
default = "vscode-web"
}
variable "folder" { variable "folder" {
type = string type = string
description = "The folder to open in vscode-web." description = "The folder to open in vscode-web."
default = "" default = ""
} }
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "log_path" { variable "log_path" {
type = string type = string
description = "The path to log." description = "The path to log."
default = "/tmp/vscode-web.log" default = "/tmp/vscode-web.log"
} }
variable "install_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
@@ -48,6 +75,22 @@ variable "accept_license" {
} }
} }
variable "telemetry_level" {
type = string
description = "Set the telemetry level for VS Code Web."
default = "error"
validation {
condition = var.telemetry_level == "off" || var.telemetry_level == "crash" || var.telemetry_level == "error" || var.telemetry_level == "all"
error_message = "Incorrect value. Please set either 'off', 'crash', 'error', or 'all'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "vscode-web" { resource "coder_script" "vscode-web" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "VS Code Web" display_name = "VS Code Web"
@@ -55,19 +98,22 @@ 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,
}) })
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 = "owner" share = var.share
order = var.order
healthcheck { healthcheck {
url = "http://localhost:${var.port}/healthz" url = "http://localhost:${var.port}/healthz"

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

@@ -1,21 +1,49 @@
#!/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 https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2)
output=$(curl -sL https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz | tar -xz -C ${INSTALL_PREFIX} --strip-components 1)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Failed to install 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
echo "👷 Running ${INSTALL_PREFIX}/bin/code-server serve-local --port ${PORT} --accept-server-license-terms serve-local --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!" echo "Check logs at ${LOG_PATH}!"
${INSTALL_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}" --accept-server-license-terms serve-local --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &