Compare commits

...

194 Commits

Author SHA1 Message Date
Michael Smith
c3eee866d1 Merge pull request #264 from coder/svg-link-patch
fix: update SVG icon URL for RDP module
2024-07-02 12:15:49 -04:00
Michael Smith
bf175a1247 fix: update SVG icon URL for RDP module
Accidentally missed one of the URL before merging the main module.
2024-07-02 12:10:22 -04:00
Michael Smith
8fd54e0e78 Merge pull request #262 from coder/web-rdp
feat: add module for Web RDP
2024-07-02 12:06:20 -04:00
Parkreiner
e8ee02c044 fix: update URL for RDP icon 2024-07-02 16:02:50 +00:00
Parkreiner
aebdc9b434 fix: update docs link 2024-07-02 15:26:40 +00:00
Parkreiner
d98bfcb20b fix: add versioning to all code snippets 2024-07-02 15:21:45 +00:00
Parkreiner
894e507bb3 fix: add verison number to rdp script 2024-07-02 15:19:16 +00:00
Parkreiner
3f8f6181e0 refactor: clean up final code 2024-07-01 20:31:43 +00:00
Parkreiner
b23d85327c refactor: try extracting main script into separate template file 2024-07-01 20:11:40 +00:00
Parkreiner
a8580fe6b9 fix: update object definition for top-level templatefile 2024-07-01 19:24:47 +00:00
Parkreiner
49f060549e fix: update TF import 2024-07-01 19:14:05 +00:00
Parkreiner
b4153a6aaa refactor: split off Windows script logic into separate file 2024-07-01 19:09:43 +00:00
Parkreiner
13a8877791 Merge branch 'web-rdp' of github.com:coder/modules into web-rdp 2024-07-01 18:57:24 +00:00
Parkreiner
fd2f91c043 fix: remove commented-out code 2024-07-01 18:56:42 +00:00
Michael Smith
c59eb0c0cc chore: add new video to README 2024-07-01 10:22:22 -04:00
Parkreiner
a381c3ee29 fix: update structure of README for linter 2024-07-01 14:14:53 +00:00
Parkreiner
d9d1be08a3 fix: update README for RDP 2024-07-01 14:05:40 +00:00
Parkreiner
7a8483d816 Merge branch 'main' into web-rdp 2024-07-01 13:57:47 +00:00
Parkreiner
ec2c8edfb2 fix: update null check and remove typo 2024-06-28 21:06:08 +00:00
Parkreiner
78f91a542a wip: revert back 2024-06-28 18:25:59 +00:00
Parkreiner
78c948094d wip: try reverting temporarily 2024-06-28 18:20:46 +00:00
Parkreiner
16f96d3693 wip: add code for triggering try/catch 2024-06-28 17:49:55 +00:00
Parkreiner
8262b29063 wip: try reformatting try/catch 2024-06-28 17:34:36 +00:00
Parkreiner
4ab72575ac fix: remove accidental uncaught code 2024-06-28 17:23:58 +00:00
Parkreiner
f369697112 wip: add try/catch block 2024-06-28 17:21:24 +00:00
Parkreiner
f82c7fd7a1 test: set up NuGet in advance 2024-06-28 16:51:03 +00:00
Parkreiner
05a20a9e1f docs: rewrite comment for clarity 2024-06-27 20:00:44 +00:00
Parkreiner
90e15cd90c fix: update string formatting logic to make tests less likely to flake from modifications 2024-06-27 19:49:16 +00:00
Parkreiner
5869eb86d4 chore: finish all initial tests 2024-06-27 19:42:23 +00:00
Parkreiner
25c90001f4 docs: add comment about how regex is set up 2024-06-27 17:28:13 +00:00
Parkreiner
6409ee2bba refactor: clean up current code 2024-06-27 17:23:01 +00:00
Parkreiner
7d366ff92a chore: add first finished test 2024-06-27 17:20:00 +00:00
Parkreiner
de00f6334f chore: add type parameter for testRequiredVariables 2024-06-26 19:00:42 +00:00
Parkreiner
264584e673 fix: make comments for test helpers exportable 2024-06-26 17:59:12 +00:00
Parkreiner
83ecba2293 wip: commit current progress 2024-06-26 17:21:39 +00:00
Parkreiner
b2807640aa wip: commit progress on main test file 2024-06-26 16:01:08 +00:00
Parkreiner
33d44fdf17 fix: remove unneeded any types 2024-06-26 16:00:57 +00:00
Parkreiner
f335cd343d fix: update type definitions for helpers 2024-06-26 16:00:40 +00:00
Parkreiner
aebf095075 refactor: clean up patch logic for clarity 2024-06-26 14:37:14 +00:00
Parkreiner
b283ac3129 docs: fix misleading typo in comment 2024-06-25 21:54:13 +00:00
Parkreiner
5f418c3253 docs: add comments about necessary double dollar signs 2024-06-25 21:51:21 +00:00
Parkreiner
b09c4cb084 fix: speed up code for filling in form 2024-06-25 21:35:53 +00:00
Parkreiner
8aff87fdf7 fix: add logic for hiding the dropdown of protocol options 2024-06-25 21:20:42 +00:00
Parkreiner
f3c30abeb4 fix: make form hiding logic run on webpage opening 2024-06-25 21:03:02 +00:00
Parkreiner
a9a75b675f fix: add more changes to opacity logic 2024-06-25 21:01:11 +00:00
Parkreiner
ef4c87e48e fix: simplify code for hiding form 2024-06-25 20:45:39 +00:00
Parkreiner
1a0a8659cc wip: update logic for hiding form to avoid whiffs 2024-06-25 20:40:44 +00:00
Parkreiner
c7a4fced4c fix: update instanceof check 2024-06-25 20:15:18 +00:00
Parkreiner
5ec1b207d1 docs: remove now-inaccurate comment 2024-06-25 19:58:56 +00:00
Parkreiner
702271133f fix: update HTML query selector 2024-06-25 19:57:48 +00:00
Parkreiner
652fc6b84f refactor: clean up form code 2024-06-25 19:55:14 +00:00
Parkreiner
8195cf4453 wip: add current code for hiding Devolutions form 2024-06-25 19:48:44 +00:00
Parkreiner
d5cfadb4e7 fix: remove template literal dollar signs 2024-06-25 17:03:54 +00:00
Parkreiner
fba0f842a9 fix: remove regex search from Select-String 2024-06-24 21:47:01 +00:00
Parkreiner
14e3fc5b6b fix: whitespace 2024-06-24 21:13:15 +00:00
Parkreiner
0b6975c266 fix: escape quotes 2024-06-24 20:41:45 +00:00
Parkreiner
d530d68b12 fix: more money, more problems 2024-06-24 20:28:44 +00:00
Parkreiner
047ccd67ca fix: dolla dolla 2024-06-24 20:24:49 +00:00
Parkreiner
c7aa8253e3 fix: dolla dolla 2024-06-24 20:22:25 +00:00
Parkreiner
452f41aa86 fix: add parenthesis 2024-06-24 20:17:31 +00:00
Parkreiner
29209d546e fix: update typo in powershell script
Co-authored-by: Asher <ash@coder.com>
2024-06-24 20:13:11 +00:00
Parkreiner
aab5e55663 fix: update script frequency 2024-06-24 20:10:22 +00:00
Parkreiner
ff96b3f653 wip: commit current progress for devolutions patch 2024-06-24 20:07:39 +00:00
Parkreiner
20795aa2b6 chore: add script file for overriding Devolutions 2024-06-24 19:37:31 +00:00
Michael Brewer
45456ab394 feat(code-server): add option to skip reinstalling extensions (#259) 2024-06-17 12:42:45 -08:00
github-actions[bot]
c652dbe320 chore: bump version to 1.0.15 in README.md files (#258)
Co-authored-by: matifali <matifali@users.noreply.github.com>
2024-06-03 22:49:24 +03:00
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
Ben
b93471a381 chore: add admin username 2024-04-24 22:39:24 +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
Ben
53083a5718 add more context on auto login 2024-04-06 20:46:50 +00:00
Ben
7de78d2ef5 add tags 2024-04-06 20:36:55 +00:00
Ben
89135671b2 fix module usage 2024-04-06 20:34:06 +00:00
Ben
ac648cc0a9 add thumbnail 2024-04-06 20:32:52 +00:00
Ben
748a180ac3 add temp link to example template 2024-04-06 20:18:58 +00:00
Ben
ec922c7c3d remove metadata for now 2024-04-06 20:13:50 +00:00
Ben
9f8eee55b2 rename script 2024-04-06 20:11:59 +00:00
Ben
0e7644b284 remove count 2024-04-06 20:05:57 +00:00
Ben
bf06e8d3ac fix agent id 2024-04-06 20:04:28 +00:00
Ben
12fd16f701 add metadata and local instructions 2024-04-06 20:01:57 +00:00
Ben
1197e6bf0d fix port typo 2024-04-06 19:51:51 +00:00
Ben
c5c521fabd feat: add web RDP module 2024-04-06 19:48:37 +00:00
Muhammad Atif Ali
838ec95875 fix(vscode-web): use --host 127.0.01 (#216)
MS code-server defaults to using `--host localhost`, which was working perfectly fine with Coder.

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

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

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

View File

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

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

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

19
.icons/airflow.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

5
.icons/desktop.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 540 B

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

View File

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

View File

@@ -3,13 +3,15 @@
To create a new module, clone this repository and run: To create a new module, clone this repository and run:
```shell ```shell
./new.sh MOUDLE_NAME ./new.sh MODULE_NAME
``` ```
## Testing a Module ## Testing a Module
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers. 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. Reference existing `*.test.ts` files for implementation.
```shell ```shell
@@ -19,7 +21,7 @@ $ bun test -t '<module>'
You can test a module locally by updating the source as follows You can test a module locally by updating the source as follows
```hcl ```tf
module "example" { module "example" {
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>" source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
} }

View File

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

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

View File

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

View File

@@ -51,11 +51,25 @@ variable "exclude" {
type = list(string) type = list(string)
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
locals { locals {
# This is a static list because the regions don't change _that_ # This is a static list because the regions don't change _that_
# frequently and including the `aws_regions` data source requires # frequently and including the `aws_regions` data source requires
# the provider, which requires a region. # the provider, which requires a region.
regions = { regions = {
"af-south-1" = {
name = "Africa (Cape Town)"
icon = "/emojis/1f1ff-1f1e6.png"
}
"ap-east-1" = {
name = "Asia Pacific (Hong Kong)"
icon = "/emojis/1f1ed-1f1f0.png"
}
"ap-northeast-1" = { "ap-northeast-1" = {
name = "Asia Pacific (Tokyo)" name = "Asia Pacific (Tokyo)"
icon = "/emojis/1f1ef-1f1f5.png" icon = "/emojis/1f1ef-1f1f5.png"
@@ -72,6 +86,10 @@ locals {
name = "Asia Pacific (Mumbai)" name = "Asia Pacific (Mumbai)"
icon = "/emojis/1f1ee-1f1f3.png" icon = "/emojis/1f1ee-1f1f3.png"
} }
"ap-south-2" = {
name = "Asia Pacific (Hyderabad)"
icon = "/emojis/1f1ee-1f1f3.png"
}
"ap-southeast-1" = { "ap-southeast-1" = {
name = "Asia Pacific (Singapore)" name = "Asia Pacific (Singapore)"
icon = "/emojis/1f1f8-1f1ec.png" icon = "/emojis/1f1f8-1f1ec.png"
@@ -80,18 +98,42 @@ locals {
name = "Asia Pacific (Sydney)" name = "Asia Pacific (Sydney)"
icon = "/emojis/1f1e6-1f1fa.png" icon = "/emojis/1f1e6-1f1fa.png"
} }
"ap-southeast-3" = {
name = "Asia Pacific (Jakarta)"
icon = "/emojis/1f1ee-1f1e9.png"
}
"ap-southeast-4" = {
name = "Asia Pacific (Melbourne)"
icon = "/emojis/1f1e6-1f1fa.png"
}
"ca-central-1" = { "ca-central-1" = {
name = "Canada (Central)" name = "Canada (Central)"
icon = "/emojis/1f1e8-1f1e6.png" icon = "/emojis/1f1e8-1f1e6.png"
} }
"ca-west-1" = {
name = "Canada West (Calgary)"
icon = "/emojis/1f1e8-1f1e6.png"
}
"eu-central-1" = { "eu-central-1" = {
name = "EU (Frankfurt)" name = "EU (Frankfurt)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
} }
"eu-central-2" = {
name = "Europe (Zurich)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-north-1" = { "eu-north-1" = {
name = "EU (Stockholm)" name = "EU (Stockholm)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
} }
"eu-south-1" = {
name = "Europe (Milan)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-south-2" = {
name = "Europe (Spain)"
icon = "/emojis/1f1ea-1f1fa.png"
}
"eu-west-1" = { "eu-west-1" = {
name = "EU (Ireland)" name = "EU (Ireland)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
@@ -104,6 +146,14 @@ locals {
name = "EU (Paris)" name = "EU (Paris)"
icon = "/emojis/1f1ea-1f1fa.png" icon = "/emojis/1f1ea-1f1fa.png"
} }
"il-central-1" = {
name = "Israel (Tel Aviv)"
icon = "/emojis/1f1ee-1f1f1.png"
}
"me-south-1" = {
name = "Middle East (Bahrain)"
icon = "/emojis/1f1e7-1f1ed.png"
}
"sa-east-1" = { "sa-east-1" = {
name = "South America (São Paulo)" name = "South America (São Paulo)"
icon = "/emojis/1f1e7-1f1f7.png" icon = "/emojis/1f1e7-1f1f7.png"
@@ -132,6 +182,7 @@ data "coder_parameter" "region" {
display_name = var.display_name display_name = var.display_name
description = var.description description = var.description
default = var.default == "" ? null : var.default default = var.default == "" ? null : var.default
order = var.coder_parameter_order
mutable = var.mutable mutable = var.mutable
dynamic "option" { dynamic "option" {
for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) } for_each = { for k, v in local.regions : k => v if !(contains(var.exclude, k)) }
@@ -145,4 +196,4 @@ data "coder_parameter" "region" {
output "value" { output "value" {
value = data.coder_parameter.region.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. This module adds a parameter with all Azure regions, allowing developers to select the region closest to them.
```hcl ```tf
module "azure_region" { module "azure_region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.0" version = "1.0.12"
default = "eastus" default = "eastus"
} }
@@ -31,15 +31,15 @@ resource "azurem_resource_group" "example" {
Change the display name and icon for a region using the corresponding maps: Change the display name and icon for a region using the corresponding maps:
```hcl ```tf
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.0" version = "1.0.12"
custom_names = { custom_names = {
"australia": "Go Australia!" "australia" : "Go Australia!"
} }
custom_icons = { 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: Hide all regions in Australia except australiacentral:
```hcl ```tf
module "azure-region" { module "azure-region" {
source = "registry.coder.com/modules/azure-region/coder" source = "registry.coder.com/modules/azure-region/coder"
version = "1.0.0" version = "1.0.12"
exclude = [ exclude = [
"australia", "australia",
"australiacentral2", "australiacentral2",

View File

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

View File

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

BIN
bun.lockb

Binary file not shown.

View File

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

View File

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

View File

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

View File

@@ -4,39 +4,19 @@ EXTENSIONS=("${EXTENSIONS}")
BOLD='\033[0;1m' BOLD='\033[0;1m'
CODE='\033[36;40;1m' CODE='\033[36;40;1m'
RESET='\033[0m' RESET='\033[0m'
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" CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
# Install each extension... # Set extension directory
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}" EXTENSION_ARG=""
for extension in "$${EXTENSIONLIST[@]}"; do if [ -n "${EXTENSIONS_DIR}" ]; then
if [ -z "$extension" ]; then EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
continue fi
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n" function run_code_server() {
output=$($CODE_SERVER --install-extension "$extension") echo "👷 Running code-server in the background..."
if [ $? -ne 0 ]; then echo "Check logs at ${LOG_PATH}!"
echo "Failed to install extension: $extension: $output" $CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
exit 1 }
fi
done
# Check if the settings file exists... # Check if the settings file exists...
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
@@ -45,6 +25,92 @@ if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
fi fi
echo "👷 Running code-server in the background..." # Check if code-server is already installed for offline
echo "Check logs at ${LOG_PATH}!" if [ "${OFFLINE}" = true ]; then
$CODE_SERVER --auth none --port ${PORT} > ${LOG_PATH} 2>&1 & 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
# Get the list of installed extensions...
LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG)
readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS"
function extension_installed() {
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then
return 1
fi
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
if [ "$_extension" == "$1" ]; then
echo "Extension $1 was already installed."
return 0
fi
done
return 1
}
# Install each extension...
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
for extension in "$${EXTENSIONLIST[@]}"; do
if [ -z "$extension" ]; then
continue
fi
if extension_installed "$extension"; then
continue
fi
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
output=$($CODE_SERVER "$EXTENSION_ARG" --force --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
if extension_installed "$extension"; then
continue
fi
$CODE_SERVER "$EXTENSION_ARG" --force --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. Automatically logs the user into Coder when creating their workspace.
```hcl ```tf
module "coder-login" { module "coder-login" {
source = "registry.coder.com/modules/coder-login/coder" source = "registry.coder.com/modules/coder-login/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```

View File

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

View File

@@ -9,12 +9,70 @@ tags: [helper]
# Dotfiles # 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" { module "dotfiles" {
source = "registry.coder.com/modules/dotfiles/coder" source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id 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.15"
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.15"
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.15"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
source = "registry.coder.com/modules/dotfiles/coder"
version = "1.0.15"
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.15"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
```

View File

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

View File

@@ -14,24 +14,55 @@ variable "agent_id" {
description = "The ID of a Coder agent." description = "The ID of a Coder agent."
} }
variable "default_dotfiles_uri" {
type = string
description = "The default dotfiles URI if the workspace user does not provide one"
default = ""
}
variable "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" { data "coder_parameter" "dotfiles_uri" {
count = var.dotfiles_uri == null ? 1 : 0
type = "string" type = "string"
name = "dotfiles_uri" name = "dotfiles_uri"
display_name = "Dotfiles URL (optional)" display_name = "Dotfiles URL"
default = "" order = var.coder_parameter_order
default = var.default_dotfiles_uri
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace" description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
mutable = true mutable = true
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
} }
resource "coder_script" "personalize" { locals {
agent_id = var.agent_id dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
script = <<-EOT user = var.user != null ? var.user : ""
DOTFILES_URI="${data.coder_parameter.dotfiles_uri.value}" }
if [ -n "$${DOTFILES_URI// }" ]; then
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee -a ~/.dotfiles.log resource "coder_script" "dotfiles" {
fi agent_id = var.agent_id
EOT script = templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user
})
display_name = "Dotfiles" display_name = "Dotfiles"
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
run_on_start = true run_on_start = true
@@ -39,5 +70,5 @@ resource "coder_script" "personalize" {
output "dotfiles_uri" { output "dotfiles_uri" {
description = "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 # exoscale-instance-type
A parameter with all Exoscale instance types. This allows developers to select A parameter with all Exoscale instance types. This allows developers to select
their desired virtuell machine for the workspace. their desired virtual machine for the workspace.
Customize the preselected parameter value: Customize the preselected parameter value:
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "standard.medium" default = "standard.medium"
} }
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {
@@ -42,22 +42,24 @@ resource "coder_metadata" "workspace_info" {
Change the display name a type using the corresponding maps: Change the display name a type using the corresponding maps:
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "standard.medium" default = "standard.medium"
custom_names = { custom_names = {
"standard.medium": "Mittlere Instanz" # German translation "standard.medium" : "Mittlere Instanz" # German translation
} }
custom_descriptions = { custom_descriptions = {
"standard.medium": "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation "standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation
} }
} }
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {
@@ -70,17 +72,17 @@ resource "coder_metadata" "workspace_info" {
![Exoscale instance types Custom](../.images/exoscale-instance-custom.png) ![Exoscale instance types Custom](../.images/exoscale-instance-custom.png)
### Use category and exlude type ### Use category and exclude type
Show only gpu1 types Show only gpu1 types
```hcl ```tf
module "exoscale-instance-type" { module "exoscale-instance-type" {
source = "registry.coder.com/modules/exoscale-instance-type/coder" source = "registry.coder.com/modules/exoscale-instance-type/coder"
version = "1.0.0" version = "1.0.12"
default = "gpu.large" default = "gpu.large"
type_category = ["gpu"] type_category = ["gpu"]
exclude = [ exclude = [
"gpu2.small", "gpu2.small",
"gpu2.medium", "gpu2.medium",
"gpu2.large", "gpu2.large",
@@ -93,8 +95,8 @@ module "exoscale-instance-type" {
} }
resource "exoscale_compute_instance" "instance" { resource "exoscale_compute_instance" "instance" {
type = module.exoscale-instance-type.value type = module.exoscale-instance-type.value
... # ...
} }
resource "coder_metadata" "workspace_info" { resource "coder_metadata" "workspace_info" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,50 @@ variable "agent_id" {
type = string type = string
} }
variable "git_providers" {
type = map(object({
provider = string
}))
description = "A mapping of URLs to their git provider."
default = {
"https://github.com/" = {
provider = "github"
},
"https://gitlab.com/" = {
provider = "gitlab"
},
}
validation {
error_message = "Allowed values for provider are \"github\" or \"gitlab\"."
condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)])
}
}
variable "branch_name" {
description = "The branch name to clone. If not provided, the default branch will be cloned."
type = string
default = ""
}
locals { 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" { output "repo_dir" {
@@ -34,11 +76,37 @@ output "repo_dir" {
description = "Full path of cloned repo directory" description = "Full path of cloned repo directory"
} }
output "git_provider" {
value = local.provider
description = "The git provider of the repository"
}
output "folder_name" {
value = local.folder_name
description = "The name of the folder that will be created"
}
output "clone_url" {
value = local.clone_url
description = "The exact Git repository URL that will be cloned"
}
output "web_url" {
value = local.web_url
description = "Git https repository URL (may be invalid for unsupported providers)"
}
output "branch_name" {
value = local.branch_name
description = "Git branch name (may be empty)"
}
resource "coder_script" "git_clone" { resource "coder_script" "git_clone" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
CLONE_PATH = local.clone_path CLONE_PATH = local.clone_path,
REPO_URL : var.url, REPO_URL : local.clone_url,
BRANCH_NAME : local.branch_name,
}) })
display_name = "Git Clone" display_name = "Git Clone"
icon = "/icon/git.svg" icon = "/icon/git.svg"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { import {
executeScriptInContainer,
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
@@ -13,31 +12,116 @@ describe("git-config", async () => {
agent_id: "foo", agent_id: "foo",
}); });
it("fails without git", async () => { it("can run apply allow_username_change and allow_email_change disabled", async () => {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
allow_username_change: "false",
allow_email_change: "false",
}); });
const output = await executeScriptInContainer(state, "alpine");
expect(output.exitCode).toBe(1); const resources = state.resources;
expect(output.stdout).toEqual([ expect(resources).toHaveLength(6);
"\u001B[0;1mChecking git-config!", expect(resources).toMatchObject([
"Git is not installed!", { 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, { const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo", agent_id: "foo",
allow_email_change: "true",
}); });
const output = await executeScriptInContainer(state, "alpine/git");
expect(output.exitCode).toBe(0); const resources = state.resources;
expect(output.stdout).toEqual([ expect(resources).toHaveLength(8);
"\u001B[0;1mChecking git-config!", expect(resources).toMatchObject([
"git-config: No user.email found, setting to ", { type: "coder_parameter", name: "user_email" },
"git-config: No user.name found, setting to default", { type: "coder_parameter", name: "username" },
"", { type: "coder_workspace", name: "me" },
"\u001B[0;1mgit-config: using email: ", { type: "coder_workspace_owner", name: "me" },
"\u001B[0;1mgit-config: using username: default", { 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 { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12" version = ">= 0.23"
} }
} }
} }
@@ -26,15 +26,22 @@ variable "allow_email_change" {
default = false default = false
} }
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "user_email" { data "coder_parameter" "user_email" {
count = var.allow_email_change ? 1 : 0 count = var.allow_email_change ? 1 : 0
name = "user_email" name = "user_email"
type = "string" type = "string"
default = "" default = ""
description = "Git user.email to be used for commits. Leave empty to default to Coder username." order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null
description = "Git user.email to be used for commits. Leave empty to default to Coder user's email."
display_name = "Git config user.email" display_name = "Git config user.email"
mutable = true mutable = true
} }
@@ -44,18 +51,34 @@ data "coder_parameter" "username" {
name = "username" name = "username"
type = "string" type = "string"
default = "" default = ""
description = "Git user.name to be used for commits. Leave empty to default to Coder username." order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null
display_name = "Git config user.name" description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
display_name = "Full Name for Git config"
mutable = true mutable = true
} }
resource "coder_script" "git_config" { resource "coder_env" "git_author_name" {
agent_id = var.agent_id agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", { name = "GIT_AUTHOR_NAME"
GIT_USERNAME = try(data.coder_parameter.username[0].value, "") == "" ? data.coder_workspace.me.owner : try(data.coder_parameter.username[0].value, "") value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_EMAIL = try(data.coder_parameter.user_email[0].value, "") == "" ? data.coder_workspace.me.owner_email : try(data.coder_parameter.user_email[0].value, "") }
})
display_name = "Git Config" resource "coder_env" "git_commmiter_name" {
icon = "/icon/git.svg" agent_id = var.agent_id
run_on_start = true name = "GIT_COMMITTER_NAME"
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_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.15"
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.15"
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. This module adds a JetBrains Gateway Button to open any workspace with a single click.
```hcl ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.0" version = "1.0.13"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
agent_name = "example"
folder = "/home/coder/example" folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS", "IU", "PY", "PS", "CL", "RM"] jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
default = "PY" default = "GO"
} }
``` ```
@@ -26,16 +27,48 @@ module "jetbrains_gateway" {
## Examples ## Examples
### Add GoLand and WebStorm with the default set to GoLand ### Add GoLand and WebStorm as options with the default set to GoLand
```hcl ```tf
module "jetbrains_gateway" { module "jetbrains_gateway" {
source = "registry.coder.com/modules/jetbrains-gateway/coder" source = "registry.coder.com/modules/jetbrains-gateway/coder"
version = "1.0.0" version = "1.0.13"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
folder = "/home/coder/example" agent_name = "example"
jetbrains_ides = ["GO", "WS"] folder = "/home/coder/example"
default = "GO" jetbrains_ides = ["GO", "WS"]
default = "GO"
}
```
### 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`) - PhpStorm (`PS`)
- CLion (`CL`) - CLion (`CL`)
- RubyMine (`RM`) - RubyMine (`RM`)
- Rider (`RD`)

View File

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

View File

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

View File

@@ -14,17 +14,18 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
![JFrog OAuth](../.images/jfrog-oauth.png) ![JFrog OAuth](../.images/jfrog-oauth.png)
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
package_managers = { package_managers = {
"npm": "npm", "npm" : "npm",
"go": "go", "go" : "go",
"pypi": "pypi" "pypi" : "pypi"
} }
} }
``` ```
@@ -34,21 +35,22 @@ module "jfrog" {
## Prerequisites ## 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 ## Examples
Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username. Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "email" username_field = "email"
package_managers = { 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. The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-oauth/coder" source = "registry.coder.com/modules/jfrog-oauth/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io" jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
configure_code_server = true # Add JFrog extension configuration for code-server configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = { package_managers = {
"npm": "npm", "npm" : "npm",
"go": "go", "go" : "go",
"pypi": "pypi" "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). JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```hcl ```tf
provider "docker" { provider "docker" {
... # ...
registry_auth { registry_auth {
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY" address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username username = module.jfrog.username
password = module.jfrog.access_token password = module.jfrog.access_token
} }

View File

@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
coder = { coder = {
source = "coder/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" { variable "username_field" {
type = string type = string
description = "The field to use for the artifactory username. i.e. Coder username or email." description = "The field to use for the artifactory username. i.e. Coder username or email."
@@ -62,11 +68,12 @@ EOF
locals { locals {
# The username field to use for artifactory # 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://", "") jfrog_host = replace(var.jfrog_url, "https://", "")
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_external_auth" "jfrog" { data "coder_external_auth" "jfrog" {
id = var.external_auth_id id = var.external_auth_id
@@ -79,8 +86,9 @@ resource "coder_script" "jfrog" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url, JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email, ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token, ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server, CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""), REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),

View File

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

View File

@@ -12,17 +12,17 @@ tags: [integration, jfrog]
Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider. Install the JF CLI and authenticate package managers with Artifactory using Artifactory terraform provider.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
package_managers = { package_managers = {
"npm": "npm", "npm" : "npm",
"go": "go", "go" : "go",
"pypi": "pypi" "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 ### Configure npm, go, and pypi to use Artifactory local repositories
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io" jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = { package_managers = {
"npm": "npm-local", "npm" : "npm-local",
"go": "go-local", "go" : "go-local",
"pypi": "pypi-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. The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
```hcl ```tf
module "jfrog" { module "jfrog" {
source = "registry.coder.com/modules/jfrog-token/coder" source = "registry.coder.com/modules/jfrog-token/coder"
version = "1.0.0" version = "1.0.15"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io" jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token artifactory_access_token = var.artifactory_access_token
configure_code_server = true # Add JFrog extension configuration for code-server configure_code_server = true # Add JFrog extension configuration for code-server
package_managers = { package_managers = {
"npm": "npm", "npm" : "npm",
"go": "go", "go" : "go",
"pypi": "pypi" "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.15"
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). JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
```hcl ```tf
provider "docker" { provider "docker" {
... # ...
registry_auth { registry_auth {
address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY" address = "https://YYYY.jfrog.io/artifactory/api/docker/REPO-KEY"
username = module.jfrog.username username = module.jfrog.username
password = module.jfrog.access_token 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 { required_providers {
coder = { coder = {
source = "coder/coder" source = "coder/coder"
version = ">= 0.12.4" version = ">= 0.23"
} }
artifactory = { artifactory = {
source = "registry.terraform.io/jfrog/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" { variable "artifactory_access_token" {
type = string type = string
description = "The admin-level access token to use for JFrog." description = "The admin-level access token to use for JFrog."
} }
variable "token_description" {
type = string
description = "Free text token description. Useful for filtering and managing tokens."
default = "Token for Coder workspace"
}
variable "check_license" { variable "check_license" {
type = bool type = bool
description = "Toggle for pre-flight checking of Artifactory license. Default to `true`." description = "Toggle for pre-flight checking of Artifactory license. Default to `true`."
@@ -83,7 +95,7 @@ EOF
locals { locals {
# The username field to use for artifactory # 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://", "") jfrog_host = replace(var.jfrog_url, "https://", "")
} }
@@ -101,9 +113,11 @@ resource "artifactory_scoped_token" "me" {
scopes = ["applied-permissions/user"] scopes = ["applied-permissions/user"]
refreshable = var.refreshable refreshable = var.refreshable
expires_in = var.expires_in expires_in = var.expires_in
description = var.token_description
} }
data "coder_workspace" "me" {} data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "jfrog" { resource "coder_script" "jfrog" {
agent_id = var.agent_id agent_id = var.agent_id
@@ -112,8 +126,9 @@ resource "coder_script" "jfrog" {
script = templatefile("${path.module}/run.sh", { script = templatefile("${path.module}/run.sh", {
JFROG_URL : var.jfrog_url, JFROG_URL : var.jfrog_url,
JFROG_HOST : local.jfrog_host, JFROG_HOST : local.jfrog_host,
JFROG_SERVER_ID : var.jfrog_server_id,
ARTIFACTORY_USERNAME : local.username, ARTIFACTORY_USERNAME : local.username,
ARTIFACTORY_EMAIL : data.coder_workspace.me.owner_email, ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token, ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
CONFIGURE_CODE_SERVER : var.configure_code_server, CONFIGURE_CODE_SERVER : var.configure_code_server,
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""), REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),

View File

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

View File

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

View File

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

View File

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

View File

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

49
lint.ts
View File

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

4
new.sh
View File

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

58
nodejs/README.md Normal file
View File

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

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

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

52
nodejs/main.tf Normal file
View File

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

51
nodejs/run.sh Executable file
View File

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

264
package-lock.json generated Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import {
runTerraformApply, runTerraformApply,
runTerraformInit, runTerraformInit,
testRequiredVariables, testRequiredVariables,
writeCoder,
} from "../test"; } from "../test";
describe("slackme", async () => { describe("slackme", async () => {
@@ -119,15 +120,6 @@ const setupContainer = async (
return { id, instance }; 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: { const assertSlackMessage = async (opts: {
command: string; command: string;
format?: 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

103
test.ts
View File

@@ -29,8 +29,10 @@ export const runContainer = async (
return containerID.trim(); return containerID.trim();
}; };
// executeScriptInContainer finds the only "coder_script" /**
// resource in the given state and runs it in a container. * Finds the only "coder_script" resource in the given state and runs it in a
* container.
*/
export const executeScriptInContainer = async ( export const executeScriptInContainer = async (
state: TerraformState, state: TerraformState,
image: string, image: string,
@@ -76,27 +78,30 @@ export const execContainer = async (
}; };
}; };
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
type TerraformStateResource = {
type: string;
name: string;
provider: string;
instances: [{ attributes: Record<string, any> }];
};
export interface TerraformState { export interface TerraformState {
outputs: { outputs: {
[key: string]: { [key: string]: {
type: string; type: string;
value: any; value: any;
}; };
} };
resources: [
{ resources: [TerraformStateResource, ...TerraformStateResource[]];
type: string;
name: string;
provider: string;
instances: [
{
attributes: {
[key: string]: any;
};
},
];
},
];
} }
export interface CoderScriptAttributes { export interface CoderScriptAttributes {
@@ -105,10 +110,11 @@ export interface CoderScriptAttributes {
url: string; url: string;
} }
// findResourceInstance finds the first instance of the given resource /**
// type in the given state. If name is specified, it will only find * finds the first instance of the given resource type in the given state. If
// the instance with the given name. * name is specified, it will only find the instance with the given name.
export const findResourceInstance = <T extends "coder_script" | string>( */
export const findResourceInstance = <T extends string>(
state: TerraformState, state: TerraformState,
type: T, type: T,
name?: string, name?: string,
@@ -131,12 +137,13 @@ export const findResourceInstance = <T extends "coder_script" | string>(
return resource.instances[0].attributes as any; return resource.instances[0].attributes as any;
}; };
// testRequiredVariables creates a test-case /**
// for each variable provided and ensures that * Creates a test-case for each variable provided and ensures that the apply
// the apply fails without it. * fails without it.
export const testRequiredVariables = ( */
export const testRequiredVariables = <TVars extends Record<string, string>>(
dir: string, dir: string,
vars: Record<string, string>, vars: TVars,
) => { ) => {
// Ensures that all required variables are provided. // Ensures that all required variables are provided.
it("required variables", async () => { it("required variables", async () => {
@@ -165,16 +172,25 @@ export const testRequiredVariables = (
}); });
}; };
// runTerraformApply runs terraform apply in the given directory /**
// with the given variables. It is fine to run in parallel with * Runs terraform apply in the given directory with the given variables. It is
// other instances of this function, as it uses a random state file. * fine to run in parallel with other instances of this function, as it uses a
export const runTerraformApply = async ( * random state file.
*/
export const runTerraformApply = async <
TVars extends Readonly<Record<string, string | boolean>>,
>(
dir: string, dir: string,
vars: Record<string, string>, vars: TVars,
env?: Record<string, string>,
): Promise<TerraformState> => { ): Promise<TerraformState> => {
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
const env = {};
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key])); const combinedEnv = env === undefined ? {} : { ...env };
for (const [key, value] of Object.entries(vars)) {
combinedEnv[`TF_VAR_${key}`] = String(value);
}
const proc = spawn( const proc = spawn(
[ [
"terraform", "terraform",
@@ -188,22 +204,26 @@ export const runTerraformApply = async (
], ],
{ {
cwd: dir, cwd: dir,
env, env: combinedEnv,
stderr: "pipe", stderr: "pipe",
stdout: "pipe", stdout: "pipe",
}, },
); );
const text = await readableStreamToText(proc.stderr); const text = await readableStreamToText(proc.stderr);
const exitCode = await proc.exited; const exitCode = await proc.exited;
if (exitCode !== 0) { if (exitCode !== 0) {
throw new Error(text); throw new Error(text);
} }
const content = await readFile(stateFile, "utf8"); const content = await readFile(stateFile, "utf8");
await unlink(stateFile); await unlink(stateFile);
return JSON.parse(content); return JSON.parse(content);
}; };
// runTerraformInit runs terraform init in the given directory. /**
* Runs terraform init in the given directory.
*/
export const runTerraformInit = async (dir: string) => { export const runTerraformInit = async (dir: string) => {
const proc = spawn(["terraform", "init"], { const proc = spawn(["terraform", "init"], {
cwd: dir, cwd: dir,
@@ -221,5 +241,14 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
status: statusCode, 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 description: Authenticates with Vault using GitHub
icon: ../.icons/vault.svg icon: ../.icons/vault.svg
maintainer_github: coder maintainer_github: coder
partner_github: hashicorp
verified: true verified: true
tags: [helper, integration, vault, github] tags: [helper, integration, vault, github]
--- ---
@@ -11,10 +12,10 @@ tags: [helper, integration, vault, github]
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using [external auth](https://coder.com/docs/v2/latest/admin/external-auth) for GitHub. This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces using [external auth](https://coder.com/docs/v2/latest/admin/external-auth) for GitHub.
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
} }
@@ -23,13 +24,13 @@ module "vault" {
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault: Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
```shell ```shell
vault kv get -mount=secret my-secret vault kv get -namespace=coder -mount=secrets coder
``` ```
or using the Vault API: or using the Vault API:
```shell ```shell
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/secret/data/my-secret" curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
``` ```
![Vault login](../.images/vault-login.png) ![Vault login](../.images/vault-login.png)
@@ -42,10 +43,10 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
### Configure Vault integration with a different Coder GitHub external auth ID (i.e., not the default `github`) ### Configure Vault integration with a different Coder GitHub external auth ID (i.e., not the default `github`)
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id" coder_github_auth_id = "my-github-auth-id"
@@ -54,10 +55,10 @@ module "vault" {
### Configure Vault integration with a different Coder GitHub external auth ID and a different Vault GitHub auth path ### Configure Vault integration with a different Coder GitHub external auth ID and a different Vault GitHub auth path
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
coder_github_auth_id = "my-github-auth-id" coder_github_auth_id = "my-github-auth-id"
@@ -67,10 +68,10 @@ module "vault" {
### Configure Vault integration and install a specific version of the Vault CLI ### Configure Vault integration and install a specific version of the Vault CLI
```hcl ```tf
module "vault" { module "vault" {
source = "registry.coder.com/modules/vault-github/coder" source = "registry.coder.com/modules/vault-github/coder"
version = "1.0.0" version = "1.0.7"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com" vault_addr = "https://vault.example.com"
vault_cli_version = "1.15.0" vault_cli_version = "1.15.0"

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

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