Compare commits

...

115 Commits

Author SHA1 Message Date
Michael Brewer
4021d856ba fix(code-server): USE_CACHED should still install extensions (#252) 2024-06-03 11:43:17 -08:00
Cian Johnston
72eaf8a9e1 Merge pull request #256 from coder/cj/deprecated_owner_fields
chore: remove usage of deprecated fields coder_workspace.owner_*
2024-05-29 20:32:23 +01:00
Cian Johnston
249cb2fe9e fmt 2024-05-29 16:30:52 +01:00
Cian Johnston
49cff4b2aa chore: remove usage of deprecated fields coder_workspace.owner_*
Updates the following modules to no longer reference fields matching
coder_workspace.owner_*:
- coder-login
- git-config
- github-upload-public-key
- jfrog-oauth
- vscode-desktop

Also updates dependency of coder/coder to 0.23.0 for the above.

For context, see https://github.com/coder/terraform-provider-coder/releases/tag/v0.23.0
2024-05-29 16:24:23 +01:00
Michael Brewer
c6b457e7fe fix(git-config): add support for coder 0.22 (#254) 2024-05-28 10:00:58 -08:00
Asher
beaa33b682 Add open_recent option to VS Code desktop (#248) 2024-05-21 09:21:04 -08:00
Phorcys
0d7bc37f9c fix(dotfiles): remove extra "(optional)" in coder parameter display name (#249) 2024-05-17 16:35:30 +03:00
github-actions[bot]
dcd605c52e chore: bump version to 1.0.14 in README.md files (#245)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-05-03 20:40:09 +03:00
Michael Brewer
f5d41520cf feat(vscode-web): add offline, use_cached, extensions_dir and auto_install_extensions (#235) 2024-05-03 09:36:28 -08:00
Garrett Delfosse
cd0c730c95 Merge pull request #241 from coder/f0ssel/github-key
feat: Add github-upload-public-key module
2024-05-02 16:28:51 -04:00
Garrett Delfosse
873207fddf remove set -e 2024-05-02 14:33:08 -04:00
Garrett Delfosse
282e1f8c57 take env and then interpolate 2024-05-02 14:21:01 -04:00
Garrett Delfosse
c068082e6b pr comments 2024-05-02 13:03:58 -04:00
Garrett Delfosse
85e73c2071 fmt 2024-05-02 12:55:52 -04:00
Garrett Delfosse
4bdb428244 fix test 2024-05-02 12:55:00 -04:00
Garrett Delfosse
daed803530 pr review 2024-05-02 12:50:36 -04:00
Garrett Delfosse
a239212f0b fmt and increase timeout again 2024-05-01 15:45:25 -04:00
Garrett Delfosse
67fef297da increase test timeout 2024-05-01 15:43:26 -04:00
Garrett Delfosse
aced7547bc fmt 2024-05-01 15:20:22 -04:00
Garrett Delfosse
36fa871e7b add tests 2024-05-01 15:19:50 -04:00
Garrett Delfosse
46bf422d61 maintainer 2024-04-30 17:14:40 +00:00
Garrett Delfosse
180e10c3ee require curl and jq 2024-04-30 17:08:48 +00:00
Garrett Delfosse
a45706ad3a fix Invalid template control keyword 2024-04-30 17:03:21 +00:00
Garrett Delfosse
5030fcb988 add coder workspace me 2024-04-30 16:59:20 +00:00
Garrett Delfosse
cff60c4a7e add auth id var 2024-04-30 16:53:47 +00:00
Garrett Delfosse
5a33af28ac fmt 2024-04-30 16:37:28 +00:00
Garrett Delfosse
428f386c4c add troubleshooting 2024-04-30 16:29:10 +00:00
Garrett Delfosse
2e43788584 heading 2024-04-30 16:23:59 +00:00
Garrett Delfosse
e8ce194ff7 use code cli for token and update readme 2024-04-30 16:23:20 +00:00
Garrett Delfosse
1273378ca8 Update README.md 2024-04-28 17:25:42 -04:00
Garrett Delfosse
edc163b5f2 fix testing 2024-04-27 18:43:38 +00:00
Garrett Delfosse
c9e418aaf5 improve status code handling and add readme 2024-04-27 18:42:08 +00:00
timquinlan
9062b4c004 Merge pull request #242 from nataindata/main
Updated readme
2024-04-26 14:54:19 -04:00
Garrett Delfosse
b2e87ef038 feat: Add github-upload-public-key module 2024-04-26 18:34:15 +00:00
nataindata
d4db52017d Updated Readme 2024-04-26 18:33:47 +00:00
NataInData
c36f4e03d7 Merge pull request #1 from coder/main
Merge from original
2024-04-26 19:30:55 +01:00
Phorcys
443485a2d7 feat(dotfiles): add ability to apply dotfiles as any user (#133)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-04-26 17:57:12 +03:00
Michael Brewer
b686f2dbd5 feat(code-server): install extensions from .vscode/extensions.json (#231) 2024-04-26 17:56:50 +03:00
timquinlan
76c60e9971 Merge pull request #240 from coder/airflow
cleaned up apache-airflow readme
2024-04-26 10:41:56 -04:00
timquinlan
b0d6224e23 cleaned up apache-airflow readme 2024-04-26 14:25:53 +00:00
github-actions[bot]
c50c4259d9 chore: bump version to 1.0.13 in README.md files (#238)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-04-25 18:53:46 +03:00
timquinlan
5f312ced5e Merge pull request #237 from coder/maintainergithub
changed maintainer_github to coder, added partner_github: nataindata
2024-04-25 11:37:48 -04:00
timquinlan
fd985bedac changed maintainer_github to coder, added partner_github: nataindata 2024-04-25 15:25:12 +00:00
timquinlan
b0c14be846 Merge pull request #236 from coder/tim-airflow
corrected path in README.md to point to modules/apache-airflow
2024-04-25 09:29:48 -04:00
timquinlan
18efe83b89 corrected path in README.md to point to modules/apache-airflow 2024-04-25 13:17:08 +00:00
Muhammad Atif Ali
33dbae6ea0 fix(jetbrains-gateway): fix icon and name of coder_app (#233) 2024-04-24 23:42:55 +03:00
timquinlan
f14e6838e4 Merge pull request #227 from nataindata/apache-airflow
Apache Airflow module
2024-04-24 12:44:48 -04:00
timquinlan
2a30982d1a Update run.sh added export and scheduler lines 2024-04-24 12:43:16 -04:00
Stephen Kirby
47e995f636 fmt 2024-04-23 20:17:21 +00:00
nataindata
56fdf096c1 Apache Airflow 2024-04-18 17:28:09 +00:00
github-actions[bot]
49df203bd6 chore: bump version to 1.0.12 in README.md files (#230)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-04-18 18:13:02 +03:00
Michael Brewer
8766c670e6 feat(git-clone): add support for tree github or gitlab clone url (#210) 2024-04-17 11:40:47 -08:00
Muhammad Atif Ali
43304e5d4e docs(jetbrains-gateway): add examples on how to use the latest version (#228) 2024-04-17 11:27:49 +03:00
Muhammad Atif Ali
d8f71e4571 feat(jetbrains-gateway): Allow fetching latest version dynamically (#226) 2024-04-17 11:05:04 +03:00
nataindata
d8102e62ec Apache Airflow module 2024-04-16 17:05:44 +00:00
Muhammad Atif Ali
ed16ba59a9 fix(dotfiles): fix typo and remove a less useful output (#225) 2024-04-15 20:31:32 +03:00
Michael Brewer
a8c659ad6f feat: add coder_parameter_order to all data.coder_parameter fields (#223) 2024-04-15 20:31:21 +03:00
Michael Brewer
c4df384f4b feat(code-server): add extension_dir variable (#205) 2024-04-14 17:14:47 +03:00
Michael Brewer
892174da7c feat(git-config): allow data.coder_workspace.me.owner_email to be blank (#222) 2024-04-14 17:10:33 +03:00
djarbz
24e50e2bbb Dotfiles template default repo (#224)
Co-authored-by: Muhammad Atif Ali <me@matifali.dev>
2024-04-14 17:06:56 +03:00
github-actions[bot]
dfe69f25ce chore: bump version to 1.0.11 in README.md files (#221)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-04-11 02:39:55 +03:00
Michael Brewer
e8f6578ece feat(jetbrains-gateway): bump version to 2024.1 (#220) 2024-04-11 02:36:25 +03:00
Muhammad Atif Ali
838ec95875 fix(vscode-web): use --host 127.0.01 (#216)
MS code-server defaults to using `--host localhost`, which was working perfectly fine with Coder.

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

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

* Add quotes and use `display_name`
2024-03-06 17:10:03 +05:00
Michael Brewer
a3dc364227 feat: add order variable to coder_app modules (#177) 2024-03-05 21:15:37 +05:00
Florian Gareis
f335a62891 Add new nodejs module (#165)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-03-05 21:13:17 +05:00
Muhammad Atif Ali
8ed13be726 chore(vault-github): update README.md (#169) 2024-03-01 16:13:32 +05:00
github-actions[bot]
b90f6f9de8 chore: bump version to 1.0.7 in README.md files (#174)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-02-26 20:53:27 +05:00
Muhammad Atif Ali
948280600a fix(vault): fix version fetching logic (#172) 2024-02-26 20:51:45 +05:00
Muhammad Atif Ali
407738b2be feat(hcp-vault-secrets): add project_id variable to HCP provider (#173) 2024-02-26 20:50:42 +05:00
github-actions[bot]
08adb4a839 chore: bump version to 1.0.6 in README.md files (#171) 2024-02-23 23:52:36 +05:00
Muhammad Atif Ali
313ec59d46 Add terraform validation to linting (#170)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-23 23:49:47 +05:00
Muhammad Atif Ali
4b04d18f39 Add extensions support for vscode-web (#154)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-23 17:02:17 +05:00
github-actions[bot]
ee53ca0281 chore: bump version to 1.0.5 in README.md files (#168)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-02-21 12:09:01 +05:00
Muhammad Atif Ali
8e254a3bb9 docs: elaborate instructions for setting up hcp vault module (#163)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-19 13:44:07 +04:00
Muhammad Atif Ali
1ab53139b3 ci: fix ci permissions (#166) 2024-02-18 23:45:33 +05:00
Muhammad Atif Ali
147bea9782 bump version to v1.0.4 (#160) 2024-02-16 18:50:30 +03:00
Victor Urvantsev
8d8910c52a feat(jfrog): add option to customize server id for JFrog CLI (#158)
Co-authored-by: Victor Urvantsev <victoru@jfrog.com>
2024-02-16 13:16:14 +03:00
Florian Gareis
c00b7536cb Add slug to code server (#161) 2024-02-16 12:54:05 +03:00
Muhammad Atif Ali
d66d7e994e ci: set base branch for docs update PR (#155) 2024-02-14 20:11:13 +03:00
Muhammad Atif Ali
d10ce91a64 fix: fix fetching rc versions of vault cli (#156)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-14 17:04:56 +03:00
Muhammad Atif Ali
534491613f Update module versions to v1.0.3 (#159) 2024-02-14 16:37:01 +03:00
Muhammad Atif Ali
ac64af6f02 Update Hashicorp vault modules (#140)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-14 01:43:33 +03:00
Muhammad Atif Ali
b299f98161 ci: automate version bumps in module README.md files (#139)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-02-13 15:23:44 +03:00
Muhammad Atif Ali
7e897a51e6 chore(vault-github): Add partner github and tests (#142) 2024-02-13 12:18:23 +03:00
Muhammad Atif Ali
ac54966f5e feat!(git-config): use full name for git configuration (#141) 2024-02-12 17:18:13 +03:00
Andrew Svoboda
aef9b3b116 Add build numbers and versions to jetbrains gateway module (#150) 2024-02-12 17:16:31 +03:00
Phorcys
a5c4d00a01 fix(git-commit-signing): fix SSH key permissions (#152) 2024-02-10 00:20:05 +03:00
Muhammad Atif Ali
3227a47044 fix(jetbrains-gateway): fix readme to include agent_name (#151) 2024-02-09 21:19:29 +03:00
Florian Gareis
cf1807dd5c Allow custom display name and slug for VS Code Web (#146) 2024-02-09 21:18:20 +03:00
Florian Gareis
4c993d342d Fix code-server docu (#147) 2024-02-09 21:17:32 +03:00
Muhammad Atif Ali
5a7e3f6ca4 Add Hashicorp Vault Secrets Integration module (#144) 2024-02-09 21:16:41 +03:00
Muhammad Atif Ali
acab6437bc chore: bump version to 1.0.2 and add script to update them automatically. (#128)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2024-01-30 17:51:31 +03:00
Muhammad Atif Ali
f16d7ca3f5 docs(jfrog-oauth): fix documentation link 2024-01-30 12:41:09 +03:00
Mathias Fredriksson
a9a58bff32 chore: lint for tf/hcl blocks (#135)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2024-01-28 00:48:49 +03:00
Muhammad Atif Ali
6b842004e6 ci: check for typos (#131) 2024-01-27 16:50:47 +03:00
Mathias Fredriksson
376c0cae31 chore: add prettier terraform formatting in markdown files (#134) 2024-01-27 15:02:40 +02:00
105 changed files with 3488 additions and 622 deletions

View File

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

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

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

19
.icons/airflow.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

1
.icons/github.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 960 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

1
.icons/node.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
.images/airflow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -11,10 +11,10 @@ tags: [helper]
<!-- Describes what this module does -->
```hcl
```tf
module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
}
```
@@ -26,11 +26,11 @@ module "MODULE_NAME" {
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```hcl
```tf
module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
]
@@ -43,13 +43,13 @@ 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:
```hcl
```tf
module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [ "dracula-theme.theme-dracula" ]
settings = {
settings = {
"workbench.colorTheme" = "Dracula"
}
}
@@ -59,11 +59,11 @@ module "MODULE_NAME" {
Run code-server in the background, don't fetch it from GitHub:
```hcl
```tf
module "MODULE_NAME" {
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.0"
source = "registry.coder.com/modules/MODULE_NAME/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
offline = true
offline = true
}
```

View File

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

View File

@@ -3,13 +3,15 @@
To create a new module, clone this repository and run:
```shell
./new.sh MOUDLE_NAME
./new.sh MODULE_NAME
```
## Testing a Module
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
Reference existing `*.test.ts` files for implementation.
```shell
@@ -19,7 +21,7 @@ $ bun test -t '<module>'
You can test a module locally by updating the source as follows
```hcl
```tf
module "example" {
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
}

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

@@ -11,10 +11,10 @@ tags: [helper, ide, web]
Automatically install [code-server](https://github.com/coder/code-server) in a workspace, create an app to access it via the dashboard, install extensions, and pre-configure editor settings.
```hcl
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
}
```
@@ -25,10 +25,10 @@ module "code-server" {
### Pin Versions
```hcl
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
version = "1.0.14"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
@@ -38,10 +38,10 @@ module "code-server" {
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
```hcl
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -55,12 +55,12 @@ 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:
```hcl
module "settings" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
extensions = [ "dracula-theme.theme-dracula" ]
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
"workbench.colorTheme" = "Dracula"
}
@@ -71,24 +71,38 @@ module "settings" {
Just run code-server in the background, don't fetch it from GitHub:
```hcl
module "settings" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
extensions = [ "dracula-theme.theme-dracula", "ms-azuretools.vscode-docker" ]
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
### Offline Mode
### Offline and Use Cached Modes
By default the module looks for code-server at `/tmp/code-server` but this can be changed with `install_prefix`.
Run an existing copy of code-server if found, otherwise download from GitHub:
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
```
Just run code-server in the background, don't fetch it from GitHub:
```hcl
module "settings" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.0"
```tf
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
offline = true
offline = true
}
```

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -32,6 +32,12 @@ variable "display_name" {
default = "code-server"
}
variable "slug" {
type = string
description = "The slug for the code-server application."
default = "code-server"
}
variable "settings" {
type = map(string)
description = "A map of settings to apply to code-server."
@@ -71,6 +77,36 @@ variable "share" {
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "offline" {
type = bool
description = "Just run code-server in the background, don't fetch it from GitHub"
default = false
}
variable "use_cached" {
type = bool
description = "Uses cached copy code-server in the background, otherwise fetched it from GitHub"
default = false
}
variable "extensions_dir" {
type = string
description = "Override the directory to store extensions in."
default = ""
}
variable "auto_install_extensions" {
type = bool
description = "Automatically install recommended extensions when code-server starts."
default = false
}
resource "coder_script" "code-server" {
agent_id = var.agent_id
display_name = "code-server"
@@ -78,23 +114,42 @@ resource "coder_script" "code-server" {
script = templatefile("${path.module}/run.sh", {
VERSION : var.install_version,
EXTENSIONS : join(",", var.extensions),
APP_NAME : var.display_name,
PORT : var.port,
LOG_PATH : var.log_path,
INSTALL_PREFIX : var.install_prefix,
// This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
})
run_on_start = true
lifecycle {
precondition {
condition = !var.offline || length(var.extensions) == 0
error_message = "Offline mode does not allow extensions to be installed"
}
precondition {
condition = !var.offline || !var.use_cached
error_message = "Offline and Use Cached can not be used together"
}
}
}
resource "coder_app" "code-server" {
agent_id = var.agent_id
slug = "code-server"
slug = var.slug
display_name = var.display_name
url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}"
icon = "/icon/code.svg"
subdomain = false
share = var.share
order = var.order
healthcheck {
url = "http://localhost:${var.port}/healthz"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

23
dotfiles/run.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -52,6 +52,12 @@ variable "share" {
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "filebrowser" {
agent_id = var.agent_id
display_name = "File Browser"
@@ -74,4 +80,5 @@ resource "coder_app" "filebrowser" {
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
subdomain = true
share = var.share
order = var.order
}

View File

@@ -5,7 +5,7 @@ printf "$${BOLD}Installing filebrowser \n\n"
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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,33 +11,145 @@ tags: [git, helper]
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" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.0"
version = "1.0.12"
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"
}
```
## Examples
### Custom Path
```hcl
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.0"
version = "1.0.12"
agent_id = coder_agent.example.id
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.12"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_git_auth" "github" {
id = "github"
}
```
## GitHub clone with branch name
To GitHub clone with a specific branch like `feat/example`
```tf
# Prompt the user for the git repo URL
data "coder_parameter" "git_repo" {
name = "git_repo"
display_name = "Git repository"
default = "https://github.com/coder/coder/tree/feat/example"
}
# Clone the repository for branch `feat/example`
module "git_clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
# Create a code-server instance for the cloned repository
module "code-server" {
source = "registry.coder.com/modules/code-server/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
order = 1
folder = "/home/${local.username}/${module.git_clone.folder_name}"
}
# Create a Coder app for the website
resource "coder_app" "website" {
agent_id = coder_agent.example.id
order = 2
slug = "website"
external = true
display_name = module.git_clone.folder_name
url = module.git_clone.web_url
icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg"
count = module.git_clone.web_url != "" ? 1 : 0
}
```
Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `github.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
## GitLab clone with branch name
To GitLab clone with a specific branch like `feat/example`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
```
Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
## Git clone with branch_name set
Alternatively, you can set the `branch_name` attribute to clone a specific branch.
For example, to clone the `feat/example` branch:
```tf
module "git-clone" {
source = "registry.coder.com/modules/git-clone/coder"
version = "1.0.12"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
}
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,8 @@ echo "Downloading SSH key"
ssh_key=$(curl --request GET \
--url "${CODER_AGENT_URL}api/v2/workspaceagents/me/gitsshkey" \
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}")
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}" \
--silent --show-error)
jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
$ssh_key
@@ -31,8 +32,8 @@ jq --raw-output ".private_key" > ~/.ssh/git-commit-signing/coder << EOF
$ssh_key
EOF
chmod -R 400 ~/.ssh/git-commit-signing/coder
chmod -R 400 ~/.ssh/git-commit-signing/coder.pub
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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

110
github-upload-public-key/run.sh Executable file
View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -14,17 +14,18 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
![JFrog OAuth](../.images/jfrog-oauth.png)
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
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"
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
@@ -34,21 +35,22 @@ module "jfrog" {
## 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](coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-oauth) on the Coder documentation.
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.
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
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"
"pypi" : "pypi"
}
}
```
@@ -67,18 +69,18 @@ pip install requests
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0"
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
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"
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
@@ -87,11 +89,11 @@ module "jfrog" {
JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```hcl
```tf
provider "docker" {
...
# ...
registry_auth {
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username
password = module.jfrog.access_token
}

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
version = ">= 0.23"
}
}
}
@@ -19,6 +19,12 @@ variable "jfrog_url" {
}
}
variable "jfrog_server_id" {
type = string
description = "The server ID of the JFrog instance for JFrog CLI configuration"
default = "0"
}
variable "username_field" {
type = string
description = "The field to use for the artifactory username. i.e. Coder username or email."
@@ -62,11 +68,12 @@ 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
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
jfrog_host = replace(var.jfrog_url, "https://", "")
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_external_auth" "jfrog" {
id = var.external_auth_id
@@ -79,8 +86,9 @@ resource "coder_script" "jfrog" {
script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.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", ""),

View File

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

View File

@@ -12,17 +12,17 @@ tags: [integration, jfrog]
Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider.
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
package_managers = {
"npm": "npm",
"go": "go",
"pypi": "pypi"
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
@@ -38,17 +38,17 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
### Configure npm, go, and pypi to use Artifactory local repositories
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
"npm": "npm-local",
"go": "go-local",
"pypi": "pypi-local"
"npm" : "npm-local",
"go" : "go-local",
"pypi" : "pypi-local"
}
}
```
@@ -71,18 +71,38 @@ pip install requests
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl
```tf
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
configure_code_server = true # Add JFrog extension configuration for code-server
configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = {
"npm": "npm",
"go": "go",
"pypi": "pypi"
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
### Add a custom token description
```tf
data "coder_workspace" "me" {}
module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.10"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}"
package_managers = {
"npm" : "npm",
"go" : "go",
"pypi" : "pypi"
}
}
```
@@ -91,14 +111,16 @@ module "jfrog" {
JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```hcl
```tf
provider "docker" {
...
# ...
registry_auth {
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY"
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username
password = module.jfrog.access_token
}
}
```
> Here `REPO_KEY` is the name of docker repository in Artifactory.

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12.4"
version = ">= 0.23"
}
artifactory = {
source = "registry.terraform.io/jfrog/artifactory"
@@ -23,11 +23,23 @@ variable "jfrog_url" {
}
}
variable "jfrog_server_id" {
type = string
description = "The server ID of the JFrog instance for JFrog CLI configuration"
default = "0"
}
variable "artifactory_access_token" {
type = string
description = "The admin-level access token to use for JFrog."
}
variable "token_description" {
type = string
description = "Free text token description. Useful for filtering and managing tokens."
default = "Token for Coder workspace"
}
variable "check_license" {
type = bool
description = "Toggle for pre-flight checking of Artifactory license. Default to `true`."
@@ -83,7 +95,7 @@ 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
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
jfrog_host = replace(var.jfrog_url, "https://", "")
}
@@ -101,9 +113,11 @@ resource "artifactory_scoped_token" "me" {
scopes = ["applied-permissions/user"]
refreshable = var.refreshable
expires_in = var.expires_in
description = var.token_description
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "jfrog" {
agent_id = var.agent_id
@@ -112,8 +126,9 @@ resource "coder_script" "jfrog" {
script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email,
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -36,6 +36,12 @@ variable "share" {
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "jupyter-notebook" {
agent_id = var.agent_id
display_name = "jupyter-notebook"
@@ -55,4 +61,5 @@ resource "coder_app" "jupyter-notebook" {
icon = "/icon/jupyter.svg"
subdomain = true
share = var.share
order = var.order
}

View File

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

View File

@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
version = ">= 0.17"
}
}
}
@@ -36,6 +36,12 @@ variable "share" {
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
resource "coder_script" "jupyterlab" {
agent_id = var.agent_id
display_name = "jupyterlab"
@@ -55,4 +61,5 @@ resource "coder_app" "jupyterlab" {
icon = "/icon/jupyter.svg"
subdomain = true
share = var.share
order = var.order
}

49
lint.ts
View File

@@ -13,9 +13,39 @@ let badExit = false;
// error reports an error to the console and sets badExit to true
// so that the process will exit with a non-zero exit code.
const error = (...data: any[]) => {
console.error(...data);
badExit = true;
}
console.error(...data);
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.
// Exits with 0 if all is good!
@@ -62,6 +92,7 @@ for (const dir of dirs) {
let h1 = false;
let code = false;
let paragraph = false;
let version = true;
for (const token of tokens) {
if (token.type === "heading" && token.depth === 1) {
@@ -77,6 +108,10 @@ for (const dir of dirs) {
}
if (token.type === "code") {
code = true;
if (token.lang === "tf" && !token.text.includes("version")) {
version = false;
error(dir.name, "missing version in tf code block");
}
continue;
}
}
@@ -89,6 +124,14 @@ for (const dir of dirs) {
if (!code) {
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) {

4
new.sh
View File

@@ -1,6 +1,6 @@
#!/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
MODULE_NAME=$1
@@ -11,7 +11,7 @@ if [ -z "$MODULE_NAME" ]; then
exit 1
fi
# Create module directory and exit if it alredy exists
# Create module directory and exit if it already exists
if [ -d "$MODULE_NAME" ]; then
echo "Module with name $MODULE_NAME already exists"
echo "Please choose a different name"

58
nodejs/README.md Normal file
View File

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

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

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

52
nodejs/main.tf Normal file
View File

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

51
nodejs/run.sh Executable file
View File

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

263
package-lock.json generated Normal file
View File

@@ -0,0 +1,263 @@
{
"name": "modules",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "modules",
"devDependencies": {
"bun-types": "^1.0.18",
"gray-matter": "^4.0.3",
"marked": "^12.0.0",
"prettier": "^3.2.5",
"prettier-plugin-sh": "^0.13.1",
"prettier-plugin-terraform-formatter": "^1.2.1"
},
"peerDependencies": {
"typescript": "^5.3.3"
}
},
"node_modules/@types/node": {
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/bun-types": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.4.tgz",
"integrity": "sha512-E1kk0FNpxpkSSlCVXEa4HfyhSUEpKtCFrybPVyz1A4TEnBGy5bqqtSYkyjKTfKScdyZTBeFrTxJLiKGOIRWgwg==",
"dev": true,
"dependencies": {
"@types/node": "~20.11.3",
"@types/ws": "~8.5.10"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"dev": true,
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"dev": true,
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/marked": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
"dev": true,
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mvdan-sh": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
"dev": true
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-sh": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
"dev": true,
"dependencies": {
"mvdan-sh": "^0.10.1",
"sh-syntax": "^0.4.1"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/prettier-plugin-terraform-formatter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
"dev": true,
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"dev": true,
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/sh-syntax": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
"dev": true,
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16.0.0"
},
"funding": {
"url": "https://opencollective.com/unts"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

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

View File

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

View File

@@ -54,11 +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`:
```hcl
```tf
module "slackme" {
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
auth_provider_id = "slack"
}
```
@@ -70,13 +70,13 @@ slackme npm run long-build
- `$COMMAND` is replaced with the command the user executed.
- `$DURATION` is replaced with a human-readable duration the command took to execute.
```hcl
```tf
module "slackme" {
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
source = "registry.coder.com/modules/slackme/coder"
version = "1.0.2"
agent_id = coder_agent.example.id
auth_provider_id = "slack"
slack_message = <<EOF
slack_message = <<EOF
👋 Hey there from Coder! $COMMAND took $DURATION to execute!
EOF
}

View File

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

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

13
test.ts
View File

@@ -171,9 +171,9 @@ export const testRequiredVariables = (
export const runTerraformApply = async (
dir: string,
vars: Record<string, string>,
env: Record<string, string> = {},
): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));
const proc = spawn(
[
@@ -222,4 +222,13 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
},
status: statusCode,
})
}
}
export const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
};

29
update-version.sh Executable file
View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
# Convert all templated variables to shell variables
INSTALL_VERSION=${INSTALL_VERSION}
VAULT_ADDR=${VAULT_ADDR}
GITHUB_EXTERNAL_AUTH_ID=${GITHUB_EXTERNAL_AUTH_ID}
AUTH_PATH=${AUTH_PATH}
@@ -21,7 +20,7 @@ fetch() {
fi
}
unzip() {
unzip_safe() {
if command -v unzip > /dev/null 2>&1; then
command unzip "$@"
elif command -v busybox > /dev/null 2>&1; then
@@ -32,57 +31,78 @@ unzip() {
fi
}
# Fetch the latest version of Vault if INSTALL_VERSION is 'latest'
if [ "$${INSTALL_VERSION}" = "latest" ]; then
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -oP 'vault/\K[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -n 1)
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"
exit 1
fi
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"
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 "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "$${VERSION}"
printf "Unsupported architecture: $${ARCH}\n"
return 1
fi
fetch vault.zip "https://releases.hashicorp.com/vault/$${VERSION}/vault_$${VERSION}_linux_amd64.zip"
if [ $? -ne 0 ]; then
printf "Failed to download Vault.\n"
exit 1
fi
unzip vault.zip
if [ $? -ne 0 ]; then
printf "Failed to unzip Vault.\n"
exit 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
mv vault ~/.local/bin/vault
if [ ! -f ~/.local/bin/vault ]; then
printf "Failed to move Vault to local bin.\n"
exit 1
# 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
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
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"
@@ -92,8 +112,6 @@ if [ $? -ne 0 ]; then
exit 1
fi
export VAULT_ADDR="$${VAULT_ADDR}"
# 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}"

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

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

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

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

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

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

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

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More