Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3eee866d1 | ||
|
|
bf175a1247 | ||
|
|
8fd54e0e78 | ||
|
|
e8ee02c044 | ||
|
|
aebdc9b434 | ||
|
|
d98bfcb20b | ||
|
|
894e507bb3 | ||
|
|
3f8f6181e0 | ||
|
|
b23d85327c | ||
|
|
a8580fe6b9 | ||
|
|
49f060549e | ||
|
|
b4153a6aaa | ||
|
|
13a8877791 | ||
|
|
fd2f91c043 | ||
|
|
c59eb0c0cc | ||
|
|
a381c3ee29 | ||
|
|
d9d1be08a3 | ||
|
|
7a8483d816 | ||
|
|
ec2c8edfb2 | ||
|
|
78f91a542a | ||
|
|
78c948094d | ||
|
|
16f96d3693 | ||
|
|
8262b29063 | ||
|
|
4ab72575ac | ||
|
|
f369697112 | ||
|
|
f82c7fd7a1 | ||
|
|
05a20a9e1f | ||
|
|
90e15cd90c | ||
|
|
5869eb86d4 | ||
|
|
25c90001f4 | ||
|
|
6409ee2bba | ||
|
|
7d366ff92a | ||
|
|
de00f6334f | ||
|
|
264584e673 | ||
|
|
83ecba2293 | ||
|
|
b2807640aa | ||
|
|
33d44fdf17 | ||
|
|
f335cd343d | ||
|
|
aebf095075 | ||
|
|
b283ac3129 | ||
|
|
5f418c3253 | ||
|
|
b09c4cb084 | ||
|
|
8aff87fdf7 | ||
|
|
f3c30abeb4 | ||
|
|
a9a75b675f | ||
|
|
ef4c87e48e | ||
|
|
1a0a8659cc | ||
|
|
c7a4fced4c | ||
|
|
5ec1b207d1 | ||
|
|
702271133f | ||
|
|
652fc6b84f | ||
|
|
8195cf4453 | ||
|
|
d5cfadb4e7 | ||
|
|
fba0f842a9 | ||
|
|
14e3fc5b6b | ||
|
|
0b6975c266 | ||
|
|
d530d68b12 | ||
|
|
047ccd67ca | ||
|
|
c7aa8253e3 | ||
|
|
452f41aa86 | ||
|
|
29209d546e | ||
|
|
aab5e55663 | ||
|
|
ff96b3f653 | ||
|
|
20795aa2b6 | ||
|
|
45456ab394 | ||
|
|
c652dbe320 | ||
|
|
4021d856ba | ||
|
|
72eaf8a9e1 | ||
|
|
249cb2fe9e | ||
|
|
49cff4b2aa | ||
|
|
c6b457e7fe | ||
|
|
beaa33b682 | ||
|
|
0d7bc37f9c | ||
|
|
dcd605c52e | ||
|
|
f5d41520cf | ||
|
|
cd0c730c95 | ||
|
|
873207fddf | ||
|
|
282e1f8c57 | ||
|
|
c068082e6b | ||
|
|
85e73c2071 | ||
|
|
4bdb428244 | ||
|
|
daed803530 | ||
|
|
a239212f0b | ||
|
|
67fef297da | ||
|
|
aced7547bc | ||
|
|
36fa871e7b | ||
|
|
46bf422d61 | ||
|
|
180e10c3ee | ||
|
|
a45706ad3a | ||
|
|
5030fcb988 | ||
|
|
cff60c4a7e | ||
|
|
5a33af28ac | ||
|
|
428f386c4c | ||
|
|
2e43788584 | ||
|
|
e8ce194ff7 | ||
|
|
1273378ca8 | ||
|
|
edc163b5f2 | ||
|
|
c9e418aaf5 | ||
|
|
9062b4c004 | ||
|
|
b2e87ef038 | ||
|
|
d4db52017d | ||
|
|
c36f4e03d7 | ||
|
|
443485a2d7 | ||
|
|
b686f2dbd5 | ||
|
|
76c60e9971 | ||
|
|
b0d6224e23 | ||
|
|
c50c4259d9 | ||
|
|
5f312ced5e | ||
|
|
fd985bedac | ||
|
|
b0c14be846 | ||
|
|
18efe83b89 | ||
|
|
b93471a381 | ||
|
|
33dbae6ea0 | ||
|
|
f14e6838e4 | ||
|
|
2a30982d1a | ||
|
|
47e995f636 | ||
|
|
56fdf096c1 | ||
|
|
49df203bd6 | ||
|
|
8766c670e6 | ||
|
|
43304e5d4e | ||
|
|
d8f71e4571 | ||
|
|
d8102e62ec | ||
|
|
ed16ba59a9 | ||
|
|
a8c659ad6f | ||
|
|
c4df384f4b | ||
|
|
892174da7c | ||
|
|
24e50e2bbb | ||
|
|
dfe69f25ce | ||
|
|
e8f6578ece | ||
|
|
53083a5718 | ||
|
|
7de78d2ef5 | ||
|
|
89135671b2 | ||
|
|
ac648cc0a9 | ||
|
|
748a180ac3 | ||
|
|
ec922c7c3d | ||
|
|
9f8eee55b2 | ||
|
|
0e7644b284 | ||
|
|
bf06e8d3ac | ||
|
|
12fd16f701 | ||
|
|
1197e6bf0d | ||
|
|
c5c521fabd | ||
|
|
838ec95875 | ||
|
|
5a0efdf867 | ||
|
|
4debc3200d | ||
|
|
5476f819ce | ||
|
|
9a5ff6df64 | ||
|
|
bab0f7d24d | ||
|
|
fc914626a2 | ||
|
|
fdbb2e30d0 | ||
|
|
ee80d1f64c | ||
|
|
017f007bde | ||
|
|
18810cc51e | ||
|
|
98a428ae89 | ||
|
|
dd072e261a | ||
|
|
7e3743739e | ||
|
|
f5681b5206 | ||
|
|
de0813f37f | ||
|
|
d8fa7c959f | ||
|
|
c3d1b4125e | ||
|
|
472d80ade6 | ||
|
|
7d6c526146 | ||
|
|
a3dc364227 | ||
|
|
f335a62891 | ||
|
|
8ed13be726 | ||
|
|
b90f6f9de8 | ||
|
|
948280600a | ||
|
|
407738b2be | ||
|
|
08adb4a839 | ||
|
|
313ec59d46 | ||
|
|
4b04d18f39 | ||
|
|
ee53ca0281 | ||
|
|
8e254a3bb9 | ||
|
|
1ab53139b3 | ||
|
|
147bea9782 | ||
|
|
8d8910c52a | ||
|
|
c00b7536cb | ||
|
|
d66d7e994e | ||
|
|
d10ce91a64 | ||
|
|
534491613f | ||
|
|
ac64af6f02 | ||
|
|
b299f98161 | ||
|
|
7e897a51e6 | ||
|
|
ac54966f5e | ||
|
|
aef9b3b116 | ||
|
|
a5c4d00a01 | ||
|
|
3227a47044 | ||
|
|
cf1807dd5c | ||
|
|
4c993d342d | ||
|
|
5a7e3f6ca4 | ||
|
|
acab6437bc | ||
|
|
f16d7ca3f5 | ||
|
|
a9a58bff32 | ||
|
|
6b842004e6 | ||
|
|
376c0cae31 |
2
.github/workflows/ci.yaml
vendored
@@ -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
@@ -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
|
After Width: | Height: | Size: 15 KiB |
5
.icons/desktop.svg
Normal 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
@@ -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 |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.8 KiB |
1
.icons/node.svg
Normal 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
|
After Width: | Height: | Size: 603 KiB |
BIN
.images/hcp-vault-secrets-credentials.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
65
apache-airflow/main.tf
Normal 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
@@ -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
|
||||||
@@ -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" {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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" {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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" {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 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" {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
....
|
# ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)) }
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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... ",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
53
github-upload-public-key/README.md
Normal 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
|
||||||
|
}
|
||||||
|
```
|
||||||
128
github-upload-public-key/main.test.ts
Normal 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;
|
||||||
|
};
|
||||||
43
github-upload-public-key/main.tf
Normal 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
@@ -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!"
|
||||||
80
hcp-vault-secrets/README.md
Normal 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.
|
||||||
|

|
||||||
|
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
@@ -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]
|
||||||
|
}
|
||||||
@@ -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`)
|
||||||
|
|||||||
@@ -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"]',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,18 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
```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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", ""),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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", ""),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ A module that adds Jupyter Notebook in your Coder template.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
```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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ A module that adds JupyterLab in your Coder template.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
```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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
package.json
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||

|

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