merge `main`
@ -0,0 +1,40 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
- name: Setup
|
||||||
|
run: bun install
|
||||||
|
- run: bun test
|
||||||
|
pretty:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
- name: Setup
|
||||||
|
run: bun install
|
||||||
|
- name: Format
|
||||||
|
run: bun fmt:ci
|
||||||
|
- name: typos-action
|
||||||
|
uses: crate-ci/typos@v1.17.2
|
||||||
|
- name: Lint
|
||||||
|
run: bun lint
|
@ -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'
|
@ -0,0 +1,4 @@
|
|||||||
|
.terraform*
|
||||||
|
node_modules
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.lock.info
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,8 @@
|
|||||||
|
<svg width="66" height="48" viewBox="0 0 66 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M64.3029 20.8302C62.9894 20.8302 62.1144 20.0449 62.1144 18.4331V9.17517C62.1144 3.26504 59.7268 0 53.5592 0H50.6941V6.24078H51.5697C53.9968 6.24078 55.1508 7.60467 55.1508 10.0431V18.2264C55.1508 21.7807 56.1853 23.2273 58.4535 23.9713C56.1853 24.6739 55.1508 26.1617 55.1508 29.716C55.1508 31.7412 55.1508 33.7663 55.1508 35.7916C55.1508 37.4861 55.1508 39.1393 54.7131 40.8337C54.2754 42.4044 53.5592 43.8922 52.5644 45.1733C52.0073 45.9174 51.3707 46.5373 50.6545 47.116V47.9425H53.5193C59.687 47.9425 62.0746 44.6774 62.0746 38.7672V29.5094C62.0746 27.8562 62.9103 27.1123 64.2634 27.1123H65.8944V20.8714H64.3029V20.8302Z" fill="#D9D9D9"/>
|
||||||
|
<path d="M44.8049 9.42443H35.9712C35.7722 9.42443 35.6131 9.25912 35.6131 9.05247V8.34987C35.6131 8.14322 35.7722 7.97791 35.9712 7.97791H44.8447C45.0436 7.97791 45.2028 8.14322 45.2028 8.34987V9.05247C45.2028 9.25912 45.0038 9.42443 44.8049 9.42443Z" fill="#D9D9D9"/>
|
||||||
|
<path d="M46.3171 18.3513H39.871C39.672 18.3513 39.5128 18.1859 39.5128 17.9792V17.2767C39.5128 17.0701 39.672 16.9047 39.871 16.9047H46.3171C46.5161 16.9047 46.6752 17.0701 46.6752 17.2767V17.9792C46.6752 18.1446 46.5161 18.3513 46.3171 18.3513Z" fill="#D9D9D9"/>
|
||||||
|
<path d="M48.8636 13.8879H35.9712C35.7722 13.8879 35.6131 13.7226 35.6131 13.5159V12.8133C35.6131 12.6067 35.7722 12.4413 35.9712 12.4413H48.8237C49.0228 12.4413 49.182 12.6067 49.182 12.8133V13.5159C49.182 13.6812 49.0626 13.8879 48.8636 13.8879Z" fill="#D9D9D9"/>
|
||||||
|
<path d="M25.7449 11.4483C26.6203 11.4483 27.4958 11.531 28.3313 11.7377V10.0431C28.3313 7.64602 29.5251 6.24078 31.9126 6.24078H32.7879V0H29.923C23.7552 0 21.3679 3.26504 21.3679 9.17517V12.2336C22.7605 11.7377 24.2329 11.4483 25.7449 11.4483Z" fill="#D9D9D9"/>
|
||||||
|
<path d="M51.5695 33.9308C50.9329 28.6819 47.0333 24.3009 42.0196 23.3089C40.6269 23.0197 39.2342 22.9783 37.8813 23.2263C37.8415 23.2263 37.8415 23.1849 37.8018 23.1849C35.6132 18.4321 30.9179 15.291 25.8246 15.291C20.7313 15.291 16.0757 18.3494 13.8474 23.1023C13.8076 23.1023 13.8076 23.1437 13.7678 23.1437C12.3353 22.9783 10.9028 23.0609 9.47035 23.433C4.5362 24.6728 0.795835 28.9711 0.119377 34.1786C0.039787 34.7159 0 35.2532 0 35.7492C0 37.3196 1.03457 38.7662 2.54664 38.9729C4.41683 39.2623 6.04827 37.7743 6.00848 35.8732C6.00848 35.5838 6.00848 35.2532 6.04827 34.9639C6.36659 32.3188 8.31638 30.087 10.863 29.467C11.6589 29.2604 12.4547 29.2191 13.2107 29.3432C15.638 29.6738 18.0255 28.3925 19.06 26.1607C19.8161 24.5075 21.0098 23.0609 22.6015 22.2757C24.3522 21.4077 26.3418 21.2838 28.1723 21.9452C30.0822 22.6477 31.5146 24.1355 32.3901 25.9953C33.3053 27.814 33.743 29.0951 35.6928 29.3432C36.4886 29.467 38.7169 29.4257 39.5526 29.3844C41.184 29.3844 42.8154 29.963 43.9694 31.1616C44.7254 31.9881 45.2825 33.0214 45.5213 34.1786C45.8793 36.0385 45.4417 37.8983 44.3673 39.3035C43.6112 40.2954 42.5767 41.0394 41.4227 41.37C40.8656 41.5354 40.3085 41.5766 39.7514 41.5766C39.4332 41.5766 38.9955 41.5766 38.4782 41.5766C36.8866 41.5766 33.5043 41.5766 30.9576 41.5766C29.2069 41.5766 27.8141 40.1302 27.8141 38.3116V26.2019C27.8141 25.7061 27.4162 25.2928 26.9387 25.2928H25.7052C23.2778 25.334 21.3281 28.1446 21.3281 31.1202C21.3281 34.096 21.3281 41.99 21.3281 41.99C21.3281 45.2137 23.8349 47.8175 26.9387 47.8175C26.9387 47.8175 40.7464 47.7761 40.9452 47.7761C44.1285 47.4454 47.0731 45.751 49.0626 43.1472C51.0522 40.6261 51.9674 37.3196 51.5695 33.9308Z" fill="#D9D9D9"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,10 @@
|
|||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 185.69 237" enable-background="new 0 0 185.69 237" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<path fill="#333333" d="M135.96,0h-1.06H16.93C7.58,0,0,7.58,0,16.93v203.14C0,229.42,7.58,237,16.93,237h151.83
|
||||||
|
c9.35,0,16.93-7.58,16.93-16.93V50.79v-1.06L135.96,0z"/>
|
||||||
|
<circle fill="#3FA9F5" cx="92.84" cy="118.5" r="25.39"/>
|
||||||
|
<line fill="#1A1A1A" x1="185.69" y1="50.79" x2="134.9" y2="0"/>
|
||||||
|
<path fill="#3FA9F5" d="M135.96,32.8c0,9.35,7.58,16.93,16.93,16.93h32.8L135.96,0V32.8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 600 B |
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68.03 68.03"><defs><style>.cls-1{fill:#da291c;}</style></defs><title>Artboard 1</title><polygon class="cls-1" points="34.02 13.31 11.27 52.72 14.52 52.72 34.02 18.94 34.02 24.57 17.77 52.72 21.02 52.72 34.02 30.2 34.02 35.83 24.27 52.72 27.52 52.72 34.02 41.46 34.02 47.09 30.77 52.72 34.02 52.72 34.02 52.72 56.77 52.72 34.02 13.31"/></svg>
|
After Width: | Height: | Size: 427 B |
@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xml:space="preserve"
|
||||||
|
width="560"
|
||||||
|
height="560"
|
||||||
|
version="1.1"
|
||||||
|
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||||
|
viewBox="0 0 560 560"
|
||||||
|
id="svg44"
|
||||||
|
sodipodi:docname="icon_raw.svg"
|
||||||
|
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||||
|
inkscape:export-filename="/home/umarcor/filebrowser/logo/icon_raw.svg.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"><metadata
|
||||||
|
id="metadata48"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1366"
|
||||||
|
inkscape:window-height="711"
|
||||||
|
id="namedview46"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.33714286"
|
||||||
|
inkscape:cx="-172.33051"
|
||||||
|
inkscape:cy="280"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="20"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg44" />
|
||||||
|
<defs
|
||||||
|
id="defs4">
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style2">
|
||||||
|
<![CDATA[
|
||||||
|
.fil1 {fill:#FEFEFE}
|
||||||
|
.fil6 {fill:#006498}
|
||||||
|
.fil7 {fill:#0EA5EB}
|
||||||
|
.fil8 {fill:#2979FF}
|
||||||
|
.fil3 {fill:#2BBCFF}
|
||||||
|
.fil0 {fill:#455A64}
|
||||||
|
.fil4 {fill:#53C6FC}
|
||||||
|
.fil5 {fill:#BDEAFF}
|
||||||
|
.fil2 {fill:#332C2B;fill-opacity:0.149020}
|
||||||
|
]]>
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
id="g85"
|
||||||
|
transform="translate(-70,-70)"><path
|
||||||
|
class="fil1"
|
||||||
|
d="M 350,71 C 504,71 629,196 629,350 629,504 504,629 350,629 196,629 71,504 71,350 71,196 196,71 350,71 Z"
|
||||||
|
id="path9"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#fefefe" /><path
|
||||||
|
class="fil2"
|
||||||
|
d="M 475,236 593,387 C 596,503 444,639 301,585 L 225,486 339,330 c 0,0 138,-95 136,-94 z"
|
||||||
|
id="path11"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#332c2b;fill-opacity:0.14902003" /><path
|
||||||
|
class="fil3"
|
||||||
|
d="m 231,211 h 208 l 38,24 v 246 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 V 219 c 0,-5 3,-8 8,-8 z"
|
||||||
|
id="path13"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#2bbcff" /><path
|
||||||
|
class="fil4"
|
||||||
|
d="m 231,211 h 208 l 38,24 v 2 L 440,214 H 231 c -4,0 -7,3 -7,7 v 263 c -1,-1 -1,-2 -1,-3 V 219 c 0,-5 3,-8 8,-8 z"
|
||||||
|
id="path15"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#53c6fc" /><polygon
|
||||||
|
class="fil5"
|
||||||
|
points="305,212 418,212 418,310 305,310 "
|
||||||
|
id="polygon17"
|
||||||
|
style="fill:#bdeaff" /><path
|
||||||
|
class="fil5"
|
||||||
|
d="m 255,363 h 189 c 3,0 5,2 5,4 V 483 H 250 V 367 c 0,-2 2,-4 5,-4 z"
|
||||||
|
id="path19"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#bdeaff" /><polygon
|
||||||
|
class="fil6"
|
||||||
|
points="250,470 449,470 449,483 250,483 "
|
||||||
|
id="polygon21"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil6"
|
||||||
|
d="m 380,226 h 10 c 3,0 6,2 6,5 v 40 c 0,3 -3,6 -6,6 h -10 c -3,0 -6,-3 -6,-6 v -40 c 0,-3 3,-5 6,-5 z"
|
||||||
|
id="path23"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil1"
|
||||||
|
d="m 254,226 c 10,0 17,7 17,17 0,9 -7,16 -17,16 -9,0 -17,-7 -17,-16 0,-10 8,-17 17,-17 z"
|
||||||
|
id="path25"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#fefefe" /><path
|
||||||
|
class="fil6"
|
||||||
|
d="m 267,448 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,3 -3,3 H 267 c -2,0 -3,-2 -3,-3 v 0 c 0,-2 1,-3 3,-3 z"
|
||||||
|
id="path27"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil6"
|
||||||
|
d="m 267,415 h 165 c 2,0 3,1 3,3 v 0 c 0,1 -1,2 -3,2 H 267 c -2,0 -3,-1 -3,-2 v 0 c 0,-2 1,-3 3,-3 z"
|
||||||
|
id="path29"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil6"
|
||||||
|
d="m 267,381 h 165 c 2,0 3,2 3,3 v 0 c 0,2 -1,3 -3,3 H 267 c -2,0 -3,-1 -3,-3 v 0 c 0,-1 1,-3 3,-3 z"
|
||||||
|
id="path31"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil1"
|
||||||
|
d="m 236,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||||
|
id="path33"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#fefefe" /><path
|
||||||
|
class="fil1"
|
||||||
|
d="m 463,472 c 3,0 5,2 5,5 0,2 -2,4 -5,4 -3,0 -5,-2 -5,-4 0,-3 2,-5 5,-5 z"
|
||||||
|
id="path35"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#fefefe" /><polygon
|
||||||
|
class="fil6"
|
||||||
|
points="305,212 284,212 284,310 305,310 "
|
||||||
|
id="polygon37"
|
||||||
|
style="fill:#006498" /><path
|
||||||
|
class="fil7"
|
||||||
|
d="m 477,479 v 2 c 0,5 -3,8 -8,8 H 231 c -5,0 -8,-3 -8,-8 v -2 c 0,4 3,8 8,8 h 238 c 5,0 8,-4 8,-8 z"
|
||||||
|
id="path39"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#0ea5eb" /><path
|
||||||
|
class="fil8"
|
||||||
|
d="M 350,70 C 505,70 630,195 630,350 630,505 505,630 350,630 195,630 70,505 70,350 70,195 195,70 350,70 Z m 0,46 C 479,116 584,221 584,350 584,479 479,584 350,584 221,584 116,479 116,350 116,221 221,116 350,116 Z"
|
||||||
|
id="path41"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#2979ff" /></g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.4 KiB |
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="97" height="97">
|
||||||
|
<path fill="#F05133" d="M92.71 44.408 52.591 4.291c-2.31-2.311-6.057-2.311-8.369 0l-8.33 8.332L46.459 23.19c2.456-.83 5.272-.273 7.229 1.685 1.969 1.97 2.521 4.81 1.67 7.275l10.186 10.185c2.465-.85 5.307-.3 7.275 1.671 2.75 2.75 2.75 7.206 0 9.958-2.752 2.751-7.208 2.751-9.961 0-2.068-2.07-2.58-5.11-1.531-7.658l-9.5-9.499v24.997c.67.332 1.303.774 1.861 1.332 2.75 2.75 2.75 7.206 0 9.959-2.75 2.749-7.209 2.749-9.957 0-2.75-2.754-2.75-7.21 0-9.959.68-.679 1.467-1.193 2.307-1.537v-25.23c-.84-.344-1.625-.853-2.307-1.537-2.083-2.082-2.584-5.14-1.516-7.698L31.798 16.715 4.288 44.222c-2.311 2.313-2.311 6.06 0 8.371l40.121 40.118c2.31 2.311 6.056 2.311 8.369 0L92.71 52.779c2.311-2.311 2.311-6.06 0-8.371z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 802 B |
@ -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 |
@ -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 |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 47.5 47.5" style="enable-background:new 0 0 47.5 47.5;" id="svg2" xml:space="preserve"><defs id="defs6"><clipPath id="clipPath18"><path d="M 0,38 38,38 38,0 0,0 0,38 z" id="path20"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,47.5)" id="g12"><g id="g14"><g clip-path="url(#clipPath18)" id="g16"><g transform="translate(21.8486,9.4102)" id="g22"><path d="m 0,0 c -0.395,-1.346 -2.46,-1.924 -4.613,-1.291 -2.153,0.632 -3.578,2.234 -3.183,3.581 0.395,1.346 2.461,1.924 4.613,1.29 C -1.029,2.949 0.396,1.347 0,0 m -2.849,24.447 c -9.941,0 -18,-6.908 -18,-15.428 0,-1.067 0.127,-2.108 0.367,-3.113 1.779,-3.06 3.01,-1.128 8.633,1.684 5.727,2.864 0,-4 -2,-8 -0.615,-1.231 -0.281,-2.272 0.56,-3.124 2.946,-1.804 6.544,-2.876 10.44,-2.876 9.942,0 18,6.907 18,15.429 0,8.52 -8.058,15.428 -18,15.428" id="path24" style="fill:#d99e82;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(14,26)" id="g26"><path d="m 0,0 c 0,-1.657 -1.343,-3 -3,-3 -1.657,0 -3,1.343 -3,3 0,1.657 1.343,3 3,3 1.657,0 3,-1.343 3,-3" id="path28" style="fill:#5c913b;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(24,28)" id="g30"><path d="m 0,0 c 0,-1.657 -1.344,-3 -3,-3 -1.657,0 -3,1.343 -3,3 0,1.657 1.343,3 3,3 1.656,0 3,-1.343 3,-3" id="path32" style="fill:#226699;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(33,22)" id="g34"><path d="m 0,0 c 0,-1.657 -1.344,-3 -3,-3 -1.656,0 -3,1.343 -3,3 0,1.657 1.344,3 3,3 1.656,0 3,-1.343 3,-3" id="path36" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g><g transform="translate(32,13)" id="g38"><path d="m 0,0 c 0,-1.656 -1.344,-3 -3,-3 -1.656,0 -3,1.344 -3,3 0,1.656 1.344,3 3,3 1.656,0 3,-1.344 3,-3" id="path40" style="fill:#ffcc4d;fill-opacity:1;fill-rule:nonzero;stroke:none"/></g></g></g></g></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="127" height="127" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z" fill="#E01E5A"/>
|
||||||
|
<path d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z" fill="#36C5F0"/>
|
||||||
|
<path d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z" fill="#2EB67D"/>
|
||||||
|
<path d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z" fill="#ECB22E"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1019 B |
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#FFD814" d="M0 0l7.971 15.516L16 0H0zm6.732 6.16h-1.27V4.89h1.27v1.27zm0-1.906h-1.27V2.985h1.27v1.269zm1.904 3.81h-1.27v-1.27h1.27v1.27zm0-1.905h-1.27V4.89h1.27v1.27zm0-1.905h-1.27V2.985h1.27v1.269zm1.894 1.905H9.26V4.89h1.27v1.27zM9.26 4.254V2.985h1.27v1.269H9.26z"/></svg>
|
After Width: | Height: | Size: 506 B |
After Width: | Height: | Size: 603 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 176 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 308 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 149 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 654 KiB |
After Width: | Height: | Size: 526 KiB |
After Width: | Height: | Size: 205 KiB |
After Width: | Height: | Size: 155 KiB |
After Width: | Height: | Size: 5.2 MiB |
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
display_name: MODULE_NAME
|
||||||
|
description: Describe what this module does
|
||||||
|
icon: ../.icons/<A_RELEVANT_ICON>.svg
|
||||||
|
maintainer_github: GITHUB_USERNAME
|
||||||
|
verified: false
|
||||||
|
tags: [helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# MODULE_NAME
|
||||||
|
|
||||||
|
<!-- Describes what this module does -->
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "MODULE_NAME" {
|
||||||
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
|
version = "1.0.2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Add a screencast or screenshot here put them in .images directory -->
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "MODULE_NAME" {
|
||||||
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
|
version = "1.0.2"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
extensions = [
|
||||||
|
"dracula-theme.theme-dracula"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter the `<author>.<name>` into the extensions array and code-server will automatically install on start.
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
|
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "MODULE_NAME" {
|
||||||
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
|
version = "1.0.2"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
extensions = [ "dracula-theme.theme-dracula" ]
|
||||||
|
settings = {
|
||||||
|
"workbench.colorTheme" = "Dracula"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3
|
||||||
|
|
||||||
|
Run code-server in the background, don't fetch it from GitHub:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "MODULE_NAME" {
|
||||||
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
|
version = "1.0.2"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
offline = true
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,108 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# A built-in icon like "/icon/code.svg" or a full URL of icon
|
||||||
|
icon_url = "https://raw.githubusercontent.com/coder/coder/main/site/static/icon/code.svg"
|
||||||
|
# a map of all possible values
|
||||||
|
options = {
|
||||||
|
"Option 1" = {
|
||||||
|
"name" = "Option 1",
|
||||||
|
"value" = "1"
|
||||||
|
"icon" = "/emojis/1.png"
|
||||||
|
}
|
||||||
|
"Option 2" = {
|
||||||
|
"name" = "Option 2",
|
||||||
|
"value" = "2"
|
||||||
|
"icon" = "/emojis/2.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 MODULE_NAME to."
|
||||||
|
default = "/tmp/MODULE_NAME.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "port" {
|
||||||
|
type = number
|
||||||
|
description = "The port to run MODULE_NAME on."
|
||||||
|
default = 19999
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mutable" {
|
||||||
|
type = bool
|
||||||
|
description = "Whether the parameter is mutable."
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
# Add other variables here
|
||||||
|
|
||||||
|
|
||||||
|
resource "coder_script" "MODULE_NAME" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "MODULE_NAME"
|
||||||
|
icon = local.icon_url
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
LOG_PATH : var.log_path,
|
||||||
|
})
|
||||||
|
run_on_start = true
|
||||||
|
run_on_stop = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "MODULE_NAME" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
slug = "MODULE_NAME"
|
||||||
|
display_name = "MODULE_NAME"
|
||||||
|
url = "http://localhost:${var.port}"
|
||||||
|
icon = local.icon_url
|
||||||
|
subdomain = false
|
||||||
|
share = "owner"
|
||||||
|
order = var.order
|
||||||
|
|
||||||
|
# Remove if the app does not have a healthcheck endpoint
|
||||||
|
healthcheck {
|
||||||
|
url = "http://localhost:${var.port}/healthz"
|
||||||
|
interval = 5
|
||||||
|
threshold = 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "MODULE_NAME" {
|
||||||
|
type = "list(string)"
|
||||||
|
name = "MODULE_NAME"
|
||||||
|
display_name = "MODULE_NAME"
|
||||||
|
icon = local.icon_url
|
||||||
|
mutable = var.mutable
|
||||||
|
default = local.options["Option 1"]["value"]
|
||||||
|
|
||||||
|
dynamic "option" {
|
||||||
|
for_each = local.options
|
||||||
|
content {
|
||||||
|
icon = option.value.icon
|
||||||
|
name = option.value.name
|
||||||
|
value = option.value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Convert templated variables to shell variables
|
||||||
|
# shellcheck disable=SC2269
|
||||||
|
LOG_PATH=${LOG_PATH}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
|
# shellcheck disable=SC2059
|
||||||
|
printf "$${BOLD}Installing MODULE_NAME ...\n\n"
|
||||||
|
|
||||||
|
# Add code here
|
||||||
|
# Use varibles from the templatefile function in main.tf
|
||||||
|
# e.g. LOG_PATH, PORT, etc.
|
||||||
|
|
||||||
|
printf "🥳 Installation comlete!\n\n"
|
||||||
|
|
||||||
|
printf "👷 Starting MODULE_NAME in background...\n\n"
|
||||||
|
# Start the app in here
|
||||||
|
# 1. Use & to run it in background
|
||||||
|
# 2. redirct stdout and stderr to log files
|
||||||
|
|
||||||
|
./app > "$${LOG_PATH}" 2>&1 &
|
||||||
|
|
||||||
|
printf "check logs at %s\n\n" "$${LOG_PATH}"
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/terraform.tfstate": true,
|
||||||
|
"**/.terraform": true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
To create a new module, clone this repository and run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./new.sh MODULE_NAME
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing a Module
|
||||||
|
|
||||||
|
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
|
||||||
|
|
||||||
|
The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
|
||||||
|
|
||||||
|
Reference existing `*.test.ts` files for implementation.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Run tests for a specific module!
|
||||||
|
$ bun test -t '<module>'
|
||||||
|
```
|
||||||
|
|
||||||
|
You can test a module locally by updating the source as follows
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "example" {
|
||||||
|
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.
|
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
@ -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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
@ -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
|
||||||
|
}
|
@ -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
|
@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("aws-region", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("azure-region", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./setup.ts"]
|
@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("code-server", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
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
|
||||||
|
});
|
@ -1,40 +1,93 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
EXTENSIONS=("${EXTENSIONS}")
|
EXTENSIONS=("${EXTENSIONS}")
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
CODE='\033[36;40;1m'
|
CODE='\033[36;40;1m'
|
||||||
RESET='\033[0m'
|
RESET='\033[0m'
|
||||||
|
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
|
||||||
|
|
||||||
|
# Set extension directory
|
||||||
|
EXTENSION_ARG=""
|
||||||
|
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||||
|
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
function run_code_server() {
|
||||||
|
echo "👷 Running code-server in the background..."
|
||||||
|
echo "Check logs at ${LOG_PATH}!"
|
||||||
|
$CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 &
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the settings file exists...
|
||||||
|
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
|
||||||
|
echo "⚙️ Creating settings file..."
|
||||||
|
mkdir -p ~/.local/share/code-server/User
|
||||||
|
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if code-server is already installed for offline or cached mode
|
||||||
|
if [ -f "$CODE_SERVER" ]; then
|
||||||
|
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
||||||
|
echo "🥳 Found a copy of code-server"
|
||||||
|
run_code_server
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Offline mode always expects a copy of code-server to be present
|
||||||
|
if [ "${OFFLINE}" = true ]; then
|
||||||
|
echo "Failed to find a copy of code-server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
printf "$${BOLD}Installing code-server!\n"
|
printf "$${BOLD}Installing code-server!\n"
|
||||||
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=${INSTALL_PREFIX})
|
|
||||||
|
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
|
if [ $? -ne 0 ]; then
|
||||||
echo "Failed to install code-server: $output"
|
echo "Failed to install code-server: $output"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
||||||
|
|
||||||
CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
|
|
||||||
|
|
||||||
# Install each extension...
|
# Install each extension...
|
||||||
for extension in "$${EXTENSIONS[@]}"; do
|
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||||
|
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||||
if [ -z "$extension" ]; then
|
if [ -z "$extension" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||||
output=$($CODE_SERVER --install-extension "$extension")
|
output=$($CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension")
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Failed to install extension: $extension: $output"
|
echo "Failed to install extension: $extension: $output"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check if the settings file exists...
|
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||||
if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
|
if ! command -v jq > /dev/null; then
|
||||||
echo "⚙️ Creating settings file..."
|
echo "jq is required to install extensions from a workspace file."
|
||||||
mkdir -p ~/.local/share/code-server/User
|
exit 0
|
||||||
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
fi
|
||||||
|
|
||||||
|
WORKSPACE_DIR="$HOME"
|
||||||
|
if [ -n "${FOLDER}" ]; then
|
||||||
|
WORKSPACE_DIR="${FOLDER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||||
|
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||||
|
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
|
||||||
|
for extension in $extensions; do
|
||||||
|
$CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension"
|
||||||
|
done
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "👷 Running code-server in the background..."
|
run_code_server
|
||||||
echo "Check logs at ${LOG_PATH}!"
|
|
||||||
$CODE_SERVER --auth none --port ${PORT} >${LOG_PATH} 2>&1 &
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
display_name: Coder Login
|
||||||
|
description: Automatically logs the user into Coder on their workspace
|
||||||
|
icon: ../.icons/coder-white.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Coder Login
|
||||||
|
|
||||||
|
Automatically logs the user into Coder when creating their workspace.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "coder-login" {
|
||||||
|
source = "registry.coder.com/modules/coder-login/coder"
|
||||||
|
version = "1.0.2"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("coder-login", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
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."
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
|
||||||
|
resource "coder_script" "coder-login" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
CODER_USER_TOKEN : data.coder_workspace.me.owner_session_token,
|
||||||
|
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
|
||||||
|
})
|
||||||
|
display_name = "Coder Login"
|
||||||
|
icon = "/icon/coder.svg"
|
||||||
|
run_on_start = true
|
||||||
|
start_blocks_login = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Automatically authenticate the user if they are not
|
||||||
|
# logged in to another deployment
|
||||||
|
|
||||||
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
|
printf "$${BOLD}Logging into Coder...\n\n$${RESET}"
|
||||||
|
|
||||||
|
if ! coder list > /dev/null 2>&1; then
|
||||||
|
set +x
|
||||||
|
coder login --token="${CODER_USER_TOKEN}" --url="${CODER_DEPLOYMENT_URL}"
|
||||||
|
else
|
||||||
|
echo "You are already authenticated with coder."
|
||||||
|
fi
|
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
display_name: Dotfiles
|
||||||
|
description: Allow developers to optionally bring their own dotfiles repository to customize their shell and IDE settings!
|
||||||
|
icon: ../.icons/dotfiles.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dotfiles
|
||||||
|
|
||||||
|
Allow developers to optionally bring their own [dotfiles repository](https://dotfiles.github.io).
|
||||||
|
|
||||||
|
This will prompt the user for their dotfiles repository URL on template creation using a `coder_parameter`.
|
||||||
|
|
||||||
|
Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/latest/dotfiles) command.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dotfiles" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Apply dotfiles as the current user
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dotfiles" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply dotfiles as another user (only works if sudo is passwordless)
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dotfiles" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
user = "root"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply the same dotfiles as the current user and root (the root dotfiles can only be applied if sudo is passwordless)
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dotfiles" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
|
||||||
|
module "dotfiles-root" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
user = "root"
|
||||||
|
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting a default dotfiles repository
|
||||||
|
|
||||||
|
You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dotfiles" {
|
||||||
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
|
version = "1.0.14"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("dotfiles", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,74 @@
|
|||||||
|
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 "default_dotfiles_uri" {
|
||||||
|
type = string
|
||||||
|
description = "The default dotfiles URI if the workspace user does not provide one"
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "dotfiles_uri" {
|
||||||
|
type = string
|
||||||
|
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
|
||||||
|
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user" {
|
||||||
|
type = string
|
||||||
|
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "coder_parameter_order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "dotfiles_uri" {
|
||||||
|
count = var.dotfiles_uri == null ? 1 : 0
|
||||||
|
|
||||||
|
type = "string"
|
||||||
|
name = "dotfiles_uri"
|
||||||
|
display_name = "Dotfiles URL (optional)"
|
||||||
|
order = var.coder_parameter_order
|
||||||
|
default = var.default_dotfiles_uri
|
||||||
|
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||||
|
mutable = true
|
||||||
|
icon = "/icon/dotfiles.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
|
||||||
|
user = var.user != null ? var.user : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "dotfiles" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
DOTFILES_URI : local.dotfiles_uri,
|
||||||
|
DOTFILES_USER : local.user
|
||||||
|
})
|
||||||
|
display_name = "Dotfiles"
|
||||||
|
icon = "/icon/dotfiles.svg"
|
||||||
|
run_on_start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "dotfiles_uri" {
|
||||||
|
description = "Dotfiles URI"
|
||||||
|
value = local.dotfiles_uri
|
||||||
|
}
|
@ -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
|
@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
display_name: exoscale-instance-type
|
||||||
|
description: A parameter with human readable exoscale instance names
|
||||||
|
icon: ../.icons/exoscale.svg
|
||||||
|
maintainer_github: WhizUs
|
||||||
|
verified: false
|
||||||
|
tags: [helper, parameter, instances, exoscale]
|
||||||
|
---
|
||||||
|
|
||||||
|
# exoscale-instance-type
|
||||||
|
|
||||||
|
A parameter with all Exoscale instance types. This allows developers to select
|
||||||
|
their desired virtual machine for the workspace.
|
||||||
|
|
||||||
|
Customize the preselected parameter value:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-instance-type" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = "standard.medium"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
type = module.exoscale-instance-type.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_metadata" "workspace_info" {
|
||||||
|
item {
|
||||||
|
key = "instance type"
|
||||||
|
value = module.exoscale-instance-type.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Customize type
|
||||||
|
|
||||||
|
Change the display name a type using the corresponding maps:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-instance-type" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = "standard.medium"
|
||||||
|
|
||||||
|
custom_names = {
|
||||||
|
"standard.medium" : "Mittlere Instanz" # German translation
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_descriptions = {
|
||||||
|
"standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
type = module.exoscale-instance-type.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_metadata" "workspace_info" {
|
||||||
|
item {
|
||||||
|
key = "instance type"
|
||||||
|
value = module.exoscale-instance-type.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Use category and exclude type
|
||||||
|
|
||||||
|
Show only gpu1 types
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-instance-type" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = "gpu.large"
|
||||||
|
type_category = ["gpu"]
|
||||||
|
exclude = [
|
||||||
|
"gpu2.small",
|
||||||
|
"gpu2.medium",
|
||||||
|
"gpu2.large",
|
||||||
|
"gpu2.huge",
|
||||||
|
"gpu3.small",
|
||||||
|
"gpu3.medium",
|
||||||
|
"gpu3.large",
|
||||||
|
"gpu3.huge"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
type = module.exoscale-instance-type.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_metadata" "workspace_info" {
|
||||||
|
item {
|
||||||
|
key = "instance type"
|
||||||
|
value = module.exoscale-instance-type.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Related templates
|
||||||
|
|
||||||
|
A related exoscale template will be provided soon.
|
@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("exoscale-instance-type", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "gpu3.huge",
|
||||||
|
type_category: `["gpu", "cpu"]`,
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("gpu3.huge");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails because of wrong categroy definition", async () => {
|
||||||
|
expect(async () => {
|
||||||
|
await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "gpu3.huge",
|
||||||
|
// type_category: ["standard"] is standard
|
||||||
|
});
|
||||||
|
}).toThrow('default value "gpu3.huge" must be defined as one of options');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,286 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "display_name" {
|
||||||
|
default = "Exoscale instance type"
|
||||||
|
description = "The display name of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "description" {
|
||||||
|
default = "Select the exoscale instance type to use for the workspace. Check out the pricing page for more information: https://www.exoscale.com/pricing"
|
||||||
|
description = "The description of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "default" {
|
||||||
|
default = ""
|
||||||
|
description = "The default instance type to use if no type is specified. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mutable" {
|
||||||
|
default = false
|
||||||
|
description = "Whether the parameter can be changed after creation."
|
||||||
|
type = bool
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "custom_names" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom display names for instance type IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
variable "custom_descriptions" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom descriptions for instance type IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "type_category" {
|
||||||
|
default = ["standard"]
|
||||||
|
description = "A list of instance type categories the user is allowed to choose. One of [\"standard\", \"cpu\", \"memory\", \"storage\", \"gpu\"]"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "exclude" {
|
||||||
|
default = []
|
||||||
|
description = "A list of instance type IDs to exclude. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "coder_parameter_order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# https://www.exoscale.com/pricing/
|
||||||
|
|
||||||
|
standard_instances = [
|
||||||
|
{
|
||||||
|
value = "standard.micro",
|
||||||
|
name = "Standard Micro",
|
||||||
|
description = "512 MB RAM, 1 Core, 10 - 200 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.tiny",
|
||||||
|
name = "Standard Tiny",
|
||||||
|
description = "1 GB RAM, 1 Core, 10 - 400 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.small",
|
||||||
|
name = "Standard Small",
|
||||||
|
description = "2 GB RAM, 2 Cores, 10 - 400 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.medium",
|
||||||
|
name = "Standard Medium",
|
||||||
|
description = "4 GB RAM, 2 Cores, 10 - 400 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.large",
|
||||||
|
name = "Standard Large",
|
||||||
|
description = "8 GB RAM, 4 Cores, 10 - 400 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.extra",
|
||||||
|
name = "Standard Extra",
|
||||||
|
description = "rge",
|
||||||
|
description = "16 GB RAM, 4 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.huge",
|
||||||
|
name = "Standard Huge",
|
||||||
|
description = "32 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.mega",
|
||||||
|
name = "Standard Mega",
|
||||||
|
description = "64 GB RAM, 12 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.titan",
|
||||||
|
name = "Standard Titan",
|
||||||
|
description = "128 GB RAM, 16 Cores, 10 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.jumbo",
|
||||||
|
name = "Standard Jumbo",
|
||||||
|
description = "256 GB RAM, 24 Cores, 10 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "standard.colossus",
|
||||||
|
name = "Standard Colossus",
|
||||||
|
description = "320 GB RAM, 40 Cores, 10 - 1.6 TB Disk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
cpu_instances = [
|
||||||
|
{
|
||||||
|
value = "cpu.extra",
|
||||||
|
name = "CPU Extra-Large",
|
||||||
|
description = "16 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "cpu.huge",
|
||||||
|
name = "CPU Huge",
|
||||||
|
description = "32 GB RAM, 16 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "cpu.mega",
|
||||||
|
name = "CPU Mega",
|
||||||
|
description = "64 GB RAM, 32 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "cpu.titan",
|
||||||
|
name = "CPU Titan",
|
||||||
|
description = "128 GB RAM, 40 Cores, 0.1 - 1.6 TB Disk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
memory_instances = [
|
||||||
|
{
|
||||||
|
value = "memory.extra",
|
||||||
|
name = "Memory Extra-Large",
|
||||||
|
description = "16 GB RAM, 2 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "memory.huge",
|
||||||
|
name = "Memory Huge",
|
||||||
|
description = "32 GB RAM, 4 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "memory.mega",
|
||||||
|
name = "Memory Mega",
|
||||||
|
description = "64 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "memory.titan",
|
||||||
|
name = "Memory Titan",
|
||||||
|
description = "128 GB RAM, 12 Cores, 0.1 - 1.6 TB Disk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
storage_instances = [
|
||||||
|
{
|
||||||
|
value = "storage.extra",
|
||||||
|
name = "Storage Extra-Large",
|
||||||
|
description = "16 GB RAM, 4 Cores, 1 - 2 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "storage.huge",
|
||||||
|
name = "Storage Huge",
|
||||||
|
description = "32 GB RAM, 8 Cores, 2 - 3 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "storage.mega",
|
||||||
|
name = "Storage Mega",
|
||||||
|
description = "64 GB RAM, 12 Cores, 3 - 5 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "storage.titan",
|
||||||
|
name = "Storage Titan",
|
||||||
|
description = "128 GB RAM, 16 Cores, 5 - 10 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "storage.jumbo",
|
||||||
|
name = "Storage Jumbo",
|
||||||
|
description = "225 GB RAM, 24 Cores, 10 - 15 TB Disk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
gpu_instances = [
|
||||||
|
{
|
||||||
|
value = "gpu.small",
|
||||||
|
name = "GPU1 Small",
|
||||||
|
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu.medium",
|
||||||
|
name = "GPU1 Medium",
|
||||||
|
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu.large",
|
||||||
|
name = "GPU1 Large",
|
||||||
|
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu.huge",
|
||||||
|
name = "GPU1 Huge",
|
||||||
|
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu2.small",
|
||||||
|
name = "GPU2 Small",
|
||||||
|
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu2.medium",
|
||||||
|
name = "GPU2 Medium",
|
||||||
|
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu2.large",
|
||||||
|
name = "GPU2 Large",
|
||||||
|
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu2.huge",
|
||||||
|
name = "GPU2 Huge",
|
||||||
|
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu3.small",
|
||||||
|
name = "GPU3 Small",
|
||||||
|
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu3.medium",
|
||||||
|
name = "GPU3 Medium",
|
||||||
|
description = "120 GB RAM, 24 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu3.large",
|
||||||
|
name = "GPU3 Large",
|
||||||
|
description = "224 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value = "gpu3.huge",
|
||||||
|
name = "GPU3 Huge",
|
||||||
|
description = "448 GB RAM, 96 Cores, 8 GPU, 0.1 - 1.6 TB Disk"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "instance_type" {
|
||||||
|
name = "exoscale_instance_type"
|
||||||
|
display_name = var.display_name
|
||||||
|
description = var.description
|
||||||
|
default = var.default == "" ? null : var.default
|
||||||
|
order = var.coder_parameter_order
|
||||||
|
mutable = var.mutable
|
||||||
|
dynamic "option" {
|
||||||
|
for_each = [for k, v in concat(
|
||||||
|
contains(var.type_category, "standard") ? local.standard_instances : [],
|
||||||
|
contains(var.type_category, "cpu") ? local.cpu_instances : [],
|
||||||
|
contains(var.type_category, "memory") ? local.memory_instances : [],
|
||||||
|
contains(var.type_category, "storage") ? local.storage_instances : [],
|
||||||
|
contains(var.type_category, "gpu") ? local.gpu_instances : []
|
||||||
|
) : v if !(contains(var.exclude, v.value))]
|
||||||
|
content {
|
||||||
|
name = try(var.custom_names[option.value.value], option.value.name)
|
||||||
|
description = try(var.custom_descriptions[option.value.value], option.value.description)
|
||||||
|
value = option.value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "value" {
|
||||||
|
value = data.coder_parameter.instance_type.value
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
display_name: exoscale-zone
|
||||||
|
description: A parameter with human zone names and icons
|
||||||
|
icon: ../.icons/exoscale.svg
|
||||||
|
maintainer_github: WhizUs
|
||||||
|
verified: false
|
||||||
|
tags: [helper, parameter, zones, regions, exoscale]
|
||||||
|
---
|
||||||
|
|
||||||
|
# exoscale-zone
|
||||||
|
|
||||||
|
A parameter with all Exoscale zones. This allows developers to select
|
||||||
|
the zone closest to them.
|
||||||
|
|
||||||
|
Customize the preselected parameter value:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-zone" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = "ch-dk-2"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data "exoscale_compute_template" "my_template" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Customize zones
|
||||||
|
|
||||||
|
Change the display name and icon for a zone using the corresponding maps:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-zone" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = "at-vie-1"
|
||||||
|
|
||||||
|
custom_names = {
|
||||||
|
"at-vie-1" : "Home Vienna"
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_icons = {
|
||||||
|
"at-vie-1" : "/emojis/1f3e0.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "exoscale_compute_template" "my_template" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Exclude regions
|
||||||
|
|
||||||
|
Hide the Switzerland zones Geneva and Zurich
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "exoscale-zone" {
|
||||||
|
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
exclude = ["ch-gva-2", "ch-dk-2"]
|
||||||
|
}
|
||||||
|
|
||||||
|
data "exoscale_compute_template" "my_template" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "exoscale_compute_instance" "instance" {
|
||||||
|
zone = module.exoscale-zone.value
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Related templates
|
||||||
|
|
||||||
|
An exoscale sample template will be delivered soon.
|
@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("exoscale-zone", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "at-vie-1",
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("at-vie-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,116 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "display_name" {
|
||||||
|
default = "Exoscale Region"
|
||||||
|
description = "The display name of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "description" {
|
||||||
|
default = "The region to deploy workspace infrastructure."
|
||||||
|
description = "The description of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "default" {
|
||||||
|
default = ""
|
||||||
|
description = "The default region to use if no region is specified."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mutable" {
|
||||||
|
default = false
|
||||||
|
description = "Whether the parameter can be changed after creation."
|
||||||
|
type = bool
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "custom_names" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom display names for region IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "custom_icons" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom icons for region IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "exclude" {
|
||||||
|
default = []
|
||||||
|
description = "A list of region IDs to exclude."
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "coder_parameter_order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# This is a static list because the zones don't change _that_
|
||||||
|
# frequently and including the `exoscale_zones` data source requires
|
||||||
|
# the provider, which requires a zone.
|
||||||
|
# https://www.exoscale.com/datacenters/
|
||||||
|
zones = {
|
||||||
|
"de-fra-1" = {
|
||||||
|
name = "Frankfurt - Germany"
|
||||||
|
icon = "/emojis/1f1e9-1f1ea.png"
|
||||||
|
}
|
||||||
|
"at-vie-1" = {
|
||||||
|
name = "Vienna 1 - Austria"
|
||||||
|
icon = "/emojis/1f1e6-1f1f9.png"
|
||||||
|
}
|
||||||
|
"at-vie-2" = {
|
||||||
|
name = "Vienna 2 - Austria"
|
||||||
|
icon = "/emojis/1f1e6-1f1f9.png"
|
||||||
|
}
|
||||||
|
"ch-gva-2" = {
|
||||||
|
name = "Geneva - Switzerland"
|
||||||
|
icon = "/emojis/1f1e8-1f1ed.png"
|
||||||
|
}
|
||||||
|
"ch-dk-2" = {
|
||||||
|
name = "Zurich - Switzerland"
|
||||||
|
icon = "/emojis/1f1e8-1f1ed.png"
|
||||||
|
}
|
||||||
|
"bg-sof-1" = {
|
||||||
|
name = "Sofia - Bulgaria"
|
||||||
|
icon = "/emojis/1f1e7-1f1ec.png"
|
||||||
|
}
|
||||||
|
"de-muc-1" = {
|
||||||
|
name = "Munich - Germany"
|
||||||
|
icon = "/emojis/1f1e9-1f1ea.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "zone" {
|
||||||
|
name = "exoscale_zone"
|
||||||
|
display_name = var.display_name
|
||||||
|
description = var.description
|
||||||
|
default = var.default == "" ? null : var.default
|
||||||
|
order = var.coder_parameter_order
|
||||||
|
mutable = var.mutable
|
||||||
|
dynamic "option" {
|
||||||
|
for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) }
|
||||||
|
content {
|
||||||
|
name = try(var.custom_names[option.key], option.value.name)
|
||||||
|
icon = try(var.custom_icons[option.key], option.value.icon)
|
||||||
|
value = option.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "value" {
|
||||||
|
value = data.coder_parameter.zone.value
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
display_name: File Browser
|
||||||
|
description: A file browser for your workspace
|
||||||
|
icon: ../.icons/filebrowser.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper, filebrowser]
|
||||||
|
---
|
||||||
|
|
||||||
|
# File Browser
|
||||||
|
|
||||||
|
A file browser for your workspace.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "filebrowser" {
|
||||||
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
version = "1.0.8"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Serve a specific directory
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "filebrowser" {
|
||||||
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
version = "1.0.8"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
folder = "/home/coder/project"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specify location of `filebrowser.db`
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "filebrowser" {
|
||||||
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
version = "1.0.8"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
database_path = ".config/filebrowser.db"
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("filebrowser", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails with wrong database_path", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
database_path: "nofb",
|
||||||
|
}).catch((e) => {
|
||||||
|
if (!e.message.startsWith("\nError: Invalid value for variable")) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs with default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
|
expect(output.exitCode).toBe(0);
|
||||||
|
expect(output.stdout).toEqual([
|
||||||
|
"\u001b[0;1mInstalling filebrowser ",
|
||||||
|
"",
|
||||||
|
"🥳 Installation complete! ",
|
||||||
|
"",
|
||||||
|
"👷 Starting filebrowser in background... ",
|
||||||
|
"",
|
||||||
|
"📂 Serving /root at http://localhost:13339 ",
|
||||||
|
"",
|
||||||
|
"Running 'filebrowser --noauth --root /root --port 13339' ",
|
||||||
|
"",
|
||||||
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs with database_path var", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
database_path: ".config/filebrowser.db",
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
|
expect(output.exitCode).toBe(0);
|
||||||
|
expect(output.stdout).toEqual([
|
||||||
|
"\u001b[0;1mInstalling filebrowser ",
|
||||||
|
"",
|
||||||
|
"🥳 Installation complete! ",
|
||||||
|
"",
|
||||||
|
"👷 Starting filebrowser in background... ",
|
||||||
|
"",
|
||||||
|
"📂 Serving /root at http://localhost:13339 ",
|
||||||
|
"",
|
||||||
|
"Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db' ",
|
||||||
|
"",
|
||||||
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs with folder var", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/home/coder/project",
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
|
expect(output.exitCode).toBe(0);
|
||||||
|
expect(output.stdout).toEqual([
|
||||||
|
"\u001B[0;1mInstalling filebrowser ",
|
||||||
|
"",
|
||||||
|
"🥳 Installation complete! ",
|
||||||
|
"",
|
||||||
|
"👷 Starting filebrowser in background... ",
|
||||||
|
"",
|
||||||
|
"📂 Serving /home/coder/project at http://localhost:13339 ",
|
||||||
|
"",
|
||||||
|
"Running 'filebrowser --noauth --root /home/coder/project --port 13339' ",
|
||||||
|
"",
|
||||||
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,84 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "database_path" {
|
||||||
|
type = string
|
||||||
|
description = "The path to the filebrowser database."
|
||||||
|
default = "filebrowser.db"
|
||||||
|
validation {
|
||||||
|
# Ensures path leads to */filebrowser.db
|
||||||
|
condition = can(regex(".*filebrowser\\.db$", var.database_path))
|
||||||
|
error_message = "The database_path must end with 'filebrowser.db'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "log_path" {
|
||||||
|
type = string
|
||||||
|
description = "The path to log filebrowser to."
|
||||||
|
default = "/tmp/filebrowser.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "port" {
|
||||||
|
type = number
|
||||||
|
description = "The port to run filebrowser on."
|
||||||
|
default = 13339
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "folder" {
|
||||||
|
type = string
|
||||||
|
description = "--root value for filebrowser."
|
||||||
|
default = "~"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "share" {
|
||||||
|
type = string
|
||||||
|
default = "owner"
|
||||||
|
validation {
|
||||||
|
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||||
|
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "order" {
|
||||||
|
type = number
|
||||||
|
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "filebrowser" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "File Browser"
|
||||||
|
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
LOG_PATH : var.log_path,
|
||||||
|
PORT : var.port,
|
||||||
|
FOLDER : var.folder,
|
||||||
|
LOG_PATH : var.log_path,
|
||||||
|
DB_PATH : var.database_path
|
||||||
|
})
|
||||||
|
run_on_start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "filebrowser" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
slug = "filebrowser"
|
||||||
|
display_name = "File Browser"
|
||||||
|
url = "http://localhost:${var.port}"
|
||||||
|
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
||||||
|
subdomain = true
|
||||||
|
share = var.share
|
||||||
|
order = var.order
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
BOLD='\033[0;1m'
|
||||||
|
printf "$${BOLD}Installing filebrowser \n\n"
|
||||||
|
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||||
|
|
||||||
|
printf "🥳 Installation complete! \n\n"
|
||||||
|
|
||||||
|
printf "👷 Starting filebrowser in background... \n\n"
|
||||||
|
|
||||||
|
ROOT_DIR=${FOLDER}
|
||||||
|
ROOT_DIR=$${ROOT_DIR/\~/$HOME}
|
||||||
|
|
||||||
|
DB_FLAG=""
|
||||||
|
if [ "${DB_PATH}" != "filebrowser.db" ]; then
|
||||||
|
DB_FLAG=" -d ${DB_PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
|
||||||
|
|
||||||
|
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
|
||||||
|
|
||||||
|
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} > ${LOG_PATH} 2>&1 &
|
||||||
|
|
||||||
|
printf "📝 Logs at ${LOG_PATH} \n\n"
|
@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("fly-region", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "atl",
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("atl");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("region filter", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
default: "atl",
|
||||||
|
regions: '["arn", "ams", "bos"]',
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,287 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "display_name" {
|
||||||
|
default = "Fly.io Region"
|
||||||
|
description = "The display name of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "description" {
|
||||||
|
default = "The region to deploy workspace infrastructure."
|
||||||
|
description = "The description of the parameter."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "default" {
|
||||||
|
default = null
|
||||||
|
description = "The default region to use if no region is specified."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "mutable" {
|
||||||
|
default = false
|
||||||
|
description = "Whether the parameter can be changed after creation."
|
||||||
|
type = bool
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "custom_names" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom display names for region IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "custom_icons" {
|
||||||
|
default = {}
|
||||||
|
description = "A map of custom icons for region IDs."
|
||||||
|
type = map(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "regions" {
|
||||||
|
default = []
|
||||||
|
description = "List of regions to include for region selection."
|
||||||
|
type = list(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
regions = {
|
||||||
|
"ams" = {
|
||||||
|
name = "Amsterdam, Netherlands"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f3-1f1f1.png"
|
||||||
|
}
|
||||||
|
"arn" = {
|
||||||
|
name = "Stockholm, Sweden"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f8-1f1ea.png"
|
||||||
|
}
|
||||||
|
"atl" = {
|
||||||
|
name = "Atlanta, Georgia (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"bog" = {
|
||||||
|
name = "Bogotá, Colombia"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e8-1f1f4.png"
|
||||||
|
}
|
||||||
|
"bom" = {
|
||||||
|
name = "Mumbai, India"
|
||||||
|
gateway = true
|
||||||
|
paid_only = true
|
||||||
|
icon = "/emojis/1f1ee-1f1f3.png"
|
||||||
|
}
|
||||||
|
"bos" = {
|
||||||
|
name = "Boston, Massachusetts (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"cdg" = {
|
||||||
|
name = "Paris, France"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1eb-1f1f7.png"
|
||||||
|
}
|
||||||
|
"den" = {
|
||||||
|
name = "Denver, Colorado (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"dfw" = {
|
||||||
|
name = "Dallas, Texas (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"ewr" = {
|
||||||
|
name = "Secaucus, NJ (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"eze" = {
|
||||||
|
name = "Ezeiza, Argentina"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e6-1f1f7.png"
|
||||||
|
}
|
||||||
|
"fra" = {
|
||||||
|
name = "Frankfurt, Germany"
|
||||||
|
gateway = true
|
||||||
|
paid_only = true
|
||||||
|
icon = "/emojis/1f1e9-1f1ea.png"
|
||||||
|
}
|
||||||
|
"gdl" = {
|
||||||
|
name = "Guadalajara, Mexico"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f2-1f1fd.png"
|
||||||
|
}
|
||||||
|
"gig" = {
|
||||||
|
name = "Rio de Janeiro, Brazil"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e7-1f1f7.png"
|
||||||
|
}
|
||||||
|
"gru" = {
|
||||||
|
name = "Sao Paulo, Brazil"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e7-1f1f7.png"
|
||||||
|
}
|
||||||
|
"hkg" = {
|
||||||
|
name = "Hong Kong, Hong Kong"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1ed-1f1f0.png"
|
||||||
|
}
|
||||||
|
"iad" = {
|
||||||
|
name = "Ashburn, Virginia (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"jnb" = {
|
||||||
|
name = "Johannesburg, South Africa"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1ff-1f1e6.png"
|
||||||
|
}
|
||||||
|
"lax" = {
|
||||||
|
name = "Los Angeles, California (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"lhr" = {
|
||||||
|
name = "London, United Kingdom"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1ec-1f1e7.png"
|
||||||
|
}
|
||||||
|
"mad" = {
|
||||||
|
name = "Madrid, Spain"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1ea-1f1f8.png"
|
||||||
|
}
|
||||||
|
"mia" = {
|
||||||
|
name = "Miami, Florida (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"nrt" = {
|
||||||
|
name = "Tokyo, Japan"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1ef-1f1f5.png"
|
||||||
|
}
|
||||||
|
"ord" = {
|
||||||
|
name = "Chicago, Illinois (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"otp" = {
|
||||||
|
name = "Bucharest, Romania"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f7-1f1f4.png"
|
||||||
|
}
|
||||||
|
"phx" = {
|
||||||
|
name = "Phoenix, Arizona (US)"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"qro" = {
|
||||||
|
name = "Querétaro, Mexico"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f2-1f1fd.png"
|
||||||
|
}
|
||||||
|
"scl" = {
|
||||||
|
name = "Santiago, Chile"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e8-1f1f1.png"
|
||||||
|
}
|
||||||
|
"sea" = {
|
||||||
|
name = "Seattle, Washington (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"sin" = {
|
||||||
|
name = "Singapore, Singapore"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f8-1f1ec.png"
|
||||||
|
}
|
||||||
|
"sjc" = {
|
||||||
|
name = "San Jose, California (US)"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1fa-1f1f8.png"
|
||||||
|
}
|
||||||
|
"syd" = {
|
||||||
|
name = "Sydney, Australia"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e6-1f1fa.png"
|
||||||
|
}
|
||||||
|
"waw" = {
|
||||||
|
name = "Warsaw, Poland"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1f5-1f1f1.png"
|
||||||
|
}
|
||||||
|
"yul" = {
|
||||||
|
name = "Montreal, Canada"
|
||||||
|
gateway = false
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e8-1f1e6.png"
|
||||||
|
}
|
||||||
|
"yyz" = {
|
||||||
|
name = "Toronto, Canada"
|
||||||
|
gateway = true
|
||||||
|
paid_only = false
|
||||||
|
icon = "/emojis/1f1e8-1f1e6.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "fly_region" {
|
||||||
|
name = "flyio_region"
|
||||||
|
display_name = var.display_name
|
||||||
|
description = var.description
|
||||||
|
default = (var.default != null && var.default != "") && ((var.default != null ? contains(var.regions, var.default) : false) || length(var.regions) == 0) ? var.default : null
|
||||||
|
mutable = var.mutable
|
||||||
|
dynamic "option" {
|
||||||
|
for_each = { for k, v in local.regions : k => v if anytrue([for d in var.regions : k == d]) || length(var.regions) == 0 }
|
||||||
|
content {
|
||||||
|
name = try(var.custom_names[option.key], option.value.name)
|
||||||
|
icon = try(var.custom_icons[option.key], option.value.icon)
|
||||||
|
value = option.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "value" {
|
||||||
|
value = data.coder_parameter.fly_region.value
|
||||||
|
}
|
@ -1,32 +1,77 @@
|
|||||||
---
|
---
|
||||||
display_name: GCP Regions
|
display_name: GCP Region
|
||||||
description: Add Google Cloud Platform regions to your Coder template.
|
description: Add Google Cloud Platform regions to your Coder template.
|
||||||
icon: ../.icons/gcp.svg
|
icon: ../.icons/gcp.svg
|
||||||
maintainer_github: coder
|
maintainer_github: coder
|
||||||
verified: true
|
verified: true
|
||||||
tags: [gcp, regions, zones, helper]
|
tags: [gcp, regions, parameter, helper]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Google Cloud Platform Regions
|
# Google Cloud Platform Regions
|
||||||
|
|
||||||
This module adds Google Cloud Platform regions to your Coder template.
|
This module adds Google Cloud Platform regions to your Coder template.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "gcp_region" {
|
||||||
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
regions = ["us", "europe"]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_instance" "example" {
|
||||||
|
zone = module.gcp_region.value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
1. Add only GPU zones in the US West 1 region:
|
### Add only GPU zones in the US West 1 region
|
||||||
|
|
||||||
|
Note: setting `gpu_only = true` and using a default region without GPU support, the default will be set to `null`.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "gcp_region" {
|
||||||
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
default = ["us-west1-a"]
|
||||||
|
regions = ["us-west1"]
|
||||||
|
gpu_only = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_instance" "example" {
|
||||||
|
zone = module.gcp_region.value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add all zones in the Europe West region
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "gcp_region" {
|
||||||
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
regions = ["europe-west"]
|
||||||
|
single_zone_per_region = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "google_compute_instance" "example" {
|
||||||
|
zone = module.gcp_region.value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```hcl
|
### Add a single zone from each region in US and Europe that has GPUs
|
||||||
module "regions" {
|
|
||||||
source = "https://registry.coder.com/modules/gcp-regions"
|
|
||||||
default = ["us-west1"]
|
|
||||||
gpu_only = true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Add all zones in the Europe West region:
|
```tf
|
||||||
|
module "gcp_region" {
|
||||||
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
regions = ["us", "europe"]
|
||||||
|
gpu_only = true
|
||||||
|
single_zone_per_region = true
|
||||||
|
}
|
||||||
|
|
||||||
```hcl
|
resource "google_compute_instance" "example" {
|
||||||
module "regions" {
|
zone = module.gcp_region.value
|
||||||
source = "https://registry.coder.com/modules/gcp-regions"
|
}
|
||||||
default = ["europe-west"]
|
```
|
||||||
}
|
|
||||||
```
|
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("gcp-region", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("customized default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
regions: '["asia"]',
|
||||||
|
default: "asia-east1-a",
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("asia-east1-a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gpu only invalid default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
regions: '["us-west2"]',
|
||||||
|
default: "us-west2-a",
|
||||||
|
gpu_only: "true",
|
||||||
|
});
|
||||||
|
expect(state.outputs.value.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gpu only valid default", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
regions: '["us-west2"]',
|
||||||
|
default: "us-west2-b",
|
||||||
|
gpu_only: "true",
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
display_name: Git Clone
|
||||||
|
description: Clone a Git repository by URL and skip if it exists.
|
||||||
|
icon: ../.icons/git.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [git, helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Clone
|
||||||
|
|
||||||
|
This module allows you to automatically clone a repository by URL and skip if it exists in the base directory provided.
|
||||||
|
|
||||||
|
```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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Custom Path
|
||||||
|
|
||||||
|
```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"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,231 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("git-clone", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
url: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails without git", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
url: "some-url",
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
|
expect(output.exitCode).toBe(1);
|
||||||
|
expect(output.stdout).toEqual(["Git is not installed!"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("runs with git", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
url: "fake-url",
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine/git");
|
||||||
|
expect(output.exitCode).toBe(128);
|
||||||
|
expect(output.stdout).toEqual([
|
||||||
|
"Creating directory ~/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...",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,115 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "url" {
|
||||||
|
description = "The URL of the Git repository."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "base_dir" {
|
||||||
|
default = ""
|
||||||
|
description = "The base directory to clone the repository. Defaults to \"$HOME\"."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "git_providers" {
|
||||||
|
type = map(object({
|
||||||
|
provider = string
|
||||||
|
}))
|
||||||
|
description = "A mapping of URLs to their git provider."
|
||||||
|
default = {
|
||||||
|
"https://github.com/" = {
|
||||||
|
provider = "github"
|
||||||
|
},
|
||||||
|
"https://gitlab.com/" = {
|
||||||
|
provider = "gitlab"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
validation {
|
||||||
|
error_message = "Allowed values for provider are \"github\" or \"gitlab\"."
|
||||||
|
condition = alltrue([for provider in var.git_providers : contains(["github", "gitlab"], provider.provider)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "branch_name" {
|
||||||
|
description = "The branch name to clone. If not provided, the default branch will be cloned."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Remove query parameters and fragments from the URL
|
||||||
|
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
||||||
|
|
||||||
|
# Find the git provider based on the URL and determine the tree path
|
||||||
|
provider_key = try(one([for key in keys(var.git_providers) : key if startswith(local.url, key)]), null)
|
||||||
|
provider = try(lookup(var.git_providers, local.provider_key).provider, "")
|
||||||
|
tree_path = local.provider == "gitlab" ? "/-/tree/" : local.provider == "github" ? "/tree/" : ""
|
||||||
|
|
||||||
|
# Remove tree and branch name from the URL
|
||||||
|
clone_url = var.branch_name == "" && local.tree_path != "" ? replace(local.url, "/${local.tree_path}.*/", "") : local.url
|
||||||
|
# Extract the branch name from the URL
|
||||||
|
branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name
|
||||||
|
# Extract the folder name from the URL
|
||||||
|
folder_name = replace(basename(local.clone_url), ".git", "")
|
||||||
|
# Construct the path to clone the repository
|
||||||
|
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
|
||||||
|
# Construct the web URL
|
||||||
|
web_url = startswith(local.clone_url, "git@") ? replace(replace(local.clone_url, ":", "/"), "git@", "https://") : local.clone_url
|
||||||
|
}
|
||||||
|
|
||||||
|
output "repo_dir" {
|
||||||
|
value = local.clone_path
|
||||||
|
description = "Full path of cloned repo directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "git_provider" {
|
||||||
|
value = local.provider
|
||||||
|
description = "The git provider of the repository"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "folder_name" {
|
||||||
|
value = local.folder_name
|
||||||
|
description = "The name of the folder that will be created"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "clone_url" {
|
||||||
|
value = local.clone_url
|
||||||
|
description = "The exact Git repository URL that will be cloned"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "web_url" {
|
||||||
|
value = local.web_url
|
||||||
|
description = "Git https repository URL (may be invalid for unsupported providers)"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "branch_name" {
|
||||||
|
value = local.branch_name
|
||||||
|
description = "Git branch name (may be empty)"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "git_clone" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
CLONE_PATH = local.clone_path,
|
||||||
|
REPO_URL : local.clone_url,
|
||||||
|
BRANCH_NAME : local.branch_name,
|
||||||
|
})
|
||||||
|
display_name = "Git Clone"
|
||||||
|
icon = "/icon/git.svg"
|
||||||
|
run_on_start = true
|
||||||
|
start_blocks_login = true
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
REPO_URL="${REPO_URL}"
|
||||||
|
CLONE_PATH="${CLONE_PATH}"
|
||||||
|
BRANCH_NAME="${BRANCH_NAME}"
|
||||||
|
# Expand home if it's specified!
|
||||||
|
CLONE_PATH="$${CLONE_PATH/#\~/$${HOME}}"
|
||||||
|
|
||||||
|
# Check if the variable is empty...
|
||||||
|
if [ -z "$REPO_URL" ]; then
|
||||||
|
echo "No repository specified!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the variable is empty...
|
||||||
|
if [ -z "$CLONE_PATH" ]; then
|
||||||
|
echo "No clone path specified!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if `git` is installed...
|
||||||
|
if ! command -v git > /dev/null; then
|
||||||
|
echo "Git is not installed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the directory for the cloning exists
|
||||||
|
# and if not, create it
|
||||||
|
if [ ! -d "$CLONE_PATH" ]; then
|
||||||
|
echo "Creating directory $CLONE_PATH..."
|
||||||
|
mkdir -p "$CLONE_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the directory is empty
|
||||||
|
# and if it is, clone the repo, otherwise skip cloning
|
||||||
|
if [ -z "$(ls -A "$CLONE_PATH")" ]; then
|
||||||
|
if [ -z "$BRANCH_NAME" ]; then
|
||||||
|
echo "Cloning $REPO_URL to $CLONE_PATH..."
|
||||||
|
git clone "$REPO_URL" "$CLONE_PATH"
|
||||||
|
else
|
||||||
|
echo "Cloning $REPO_URL to $CLONE_PATH on branch $BRANCH_NAME..."
|
||||||
|
git clone "$REPO_URL" -b "$BRANCH_NAME" "$CLONE_PATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$CLONE_PATH already exists and isn't empty, skipping clone!"
|
||||||
|
exit 0
|
||||||
|
fi
|
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
display_name: Git commit signing
|
||||||
|
description: Configures Git to sign commits using your Coder SSH key
|
||||||
|
icon: ../.icons/git.svg
|
||||||
|
maintainer_github: phorcys420
|
||||||
|
verified: false
|
||||||
|
tags: [helper, git]
|
||||||
|
---
|
||||||
|
|
||||||
|
# git-commit-signing
|
||||||
|
|
||||||
|
This module downloads your SSH key from Coder and uses it to sign commits with Git.
|
||||||
|
It requires `curl` and `jq` to be installed inside your workspace.
|
||||||
|
|
||||||
|
Please observe that using the SSH key that's part of your Coder account for commit signing, means that in the event of a breach of your Coder account, or a malicious admin, someone could perform commit signing pretending to be you.
|
||||||
|
|
||||||
|
This module has a chance of conflicting with the user's dotfiles / the personalize module if one of those has configuration directives that overwrite this module's / each other's git configuration.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "git-commit-signing" {
|
||||||
|
source = "registry.coder.com/modules/git-commit-signing/coder"
|
||||||
|
version = "1.0.11"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,25 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "git-commit-signing" {
|
||||||
|
display_name = "Git commit signing"
|
||||||
|
icon = "/icon/git.svg"
|
||||||
|
|
||||||
|
script = file("${path.module}/run.sh")
|
||||||
|
run_on_start = true
|
||||||
|
|
||||||
|
agent_id = var.agent_id
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
if ! command -v git > /dev/null; then
|
||||||
|
echo "git is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl > /dev/null; then
|
||||||
|
echo "curl is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq > /dev/null; then
|
||||||
|
echo "jq is not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh/git-commit-signing
|
||||||
|
|
||||||
|
echo "Downloading SSH key"
|
||||||
|
|
||||||
|
ssh_key=$(curl --request GET \
|
||||||
|
--url "${CODER_AGENT_URL}api/v2/workspaceagents/me/gitsshkey" \
|
||||||
|
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}" \
|
||||||
|
--silent --show-error)
|
||||||
|
|
||||||
|
jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
|
||||||
|
$ssh_key
|
||||||
|
EOF
|
||||||
|
|
||||||
|
jq --raw-output ".private_key" > ~/.ssh/git-commit-signing/coder << EOF
|
||||||
|
$ssh_key
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod -R 600 ~/.ssh/git-commit-signing/coder
|
||||||
|
chmod -R 644 ~/.ssh/git-commit-signing/coder.pub
|
||||||
|
|
||||||
|
echo "Configuring git to use the SSH key"
|
||||||
|
|
||||||
|
git config --global gpg.format ssh
|
||||||
|
git config --global commit.gpgsign true
|
||||||
|
git config --global user.signingkey ~/.ssh/git-commit-signing/coder
|
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
display_name: Git Config
|
||||||
|
description: Stores Git configuration from Coder credentials
|
||||||
|
icon: ../.icons/git.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper, git]
|
||||||
|
---
|
||||||
|
|
||||||
|
# git-config
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "git-config" {
|
||||||
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
TODO: Add screenshot
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Allow users to override both username and email
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "git-config" {
|
||||||
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
allow_email_change = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
TODO: Add screenshot
|
||||||
|
|
||||||
|
## Disallowing users from overriding both username and email
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "git-config" {
|
||||||
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
|
version = "1.0.12"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
allow_username_change = false
|
||||||
|
allow_email_change = false
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("git-config", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can run apply allow_username_change and allow_email_change disabled", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
allow_username_change: "false",
|
||||||
|
allow_email_change: "false",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resources = state.resources;
|
||||||
|
expect(resources).toHaveLength(3);
|
||||||
|
expect(resources).toMatchObject([
|
||||||
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ 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_email_change: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resources = state.resources;
|
||||||
|
expect(resources).toHaveLength(5);
|
||||||
|
expect(resources).toMatchObject([
|
||||||
|
{ type: "coder_parameter", name: "user_email" },
|
||||||
|
{ type: "coder_parameter", name: "username" },
|
||||||
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can run apply allow_email_change enabled", async () => {
|
||||||
|
const state = await runTerraformApply(
|
||||||
|
import.meta.dir,
|
||||||
|
{
|
||||||
|
agent_id: "foo",
|
||||||
|
allow_username_change: "false",
|
||||||
|
allow_email_change: "false",
|
||||||
|
},
|
||||||
|
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@emai.com" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const resources = state.resources;
|
||||||
|
expect(resources).toHaveLength(5);
|
||||||
|
expect(resources).toMatchObject([
|
||||||
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("set custom order for coder_parameter for both fields", async () => {
|
||||||
|
const order = 20;
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
allow_username_change: "true",
|
||||||
|
allow_email_change: "true",
|
||||||
|
coder_parameter_order: order.toString(),
|
||||||
|
});
|
||||||
|
expect(state.resources).toHaveLength(5);
|
||||||
|
// user_email order is the same as the order
|
||||||
|
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||||
|
// username order is incremented by 1
|
||||||
|
// @ts-ignore: Object is possibly 'null'.
|
||||||
|
expect(state.resources[1].instances[0]?.attributes.order).toBe(order + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("set custom order for coder_parameter for just username", async () => {
|
||||||
|
const order = 30;
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
allow_email_change: "false",
|
||||||
|
allow_username_change: "true",
|
||||||
|
coder_parameter_order: order.toString(),
|
||||||
|
});
|
||||||
|
expect(state.resources).toHaveLength(4);
|
||||||
|
// user_email was not created
|
||||||
|
// username order is incremented by 1
|
||||||
|
expect(state.resources[0].instances[0].attributes.order).toBe(order + 1);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,83 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.13"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allow_username_change" {
|
||||||
|
type = bool
|
||||||
|
description = "Allow developers to change their git username."
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "allow_email_change" {
|
||||||
|
type = bool
|
||||||
|
description = "Allow developers to change their git email."
|
||||||
|
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_parameter" "user_email" {
|
||||||
|
count = var.allow_email_change ? 1 : 0
|
||||||
|
name = "user_email"
|
||||||
|
type = "string"
|
||||||
|
default = ""
|
||||||
|
order = var.coder_parameter_order != null ? var.coder_parameter_order + 0 : null
|
||||||
|
description = "Git user.email to be used for commits. Leave empty to default to Coder user's email."
|
||||||
|
display_name = "Git config user.email"
|
||||||
|
mutable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "username" {
|
||||||
|
count = var.allow_username_change ? 1 : 0
|
||||||
|
name = "username"
|
||||||
|
type = "string"
|
||||||
|
default = ""
|
||||||
|
order = var.coder_parameter_order != null ? var.coder_parameter_order + 1 : null
|
||||||
|
description = "Git user.name to be used for commits. Leave empty to default to Coder user's Full Name."
|
||||||
|
display_name = "Full Name for Git config"
|
||||||
|
mutable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_env" "git_author_name" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
name = "GIT_AUTHOR_NAME"
|
||||||
|
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_env" "git_commmiter_name" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
name = "GIT_COMMITTER_NAME"
|
||||||
|
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_env" "git_author_email" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
name = "GIT_AUTHOR_EMAIL"
|
||||||
|
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
||||||
|
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_env" "git_commmiter_email" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
name = "GIT_COMMITTER_EMAIL"
|
||||||
|
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
||||||
|
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
|
||||||
|
}
|