Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83a82345de | |||
| fae52150cc | |||
| ae6cf8c366 | |||
| 04669ec2fa | |||
| 55c9829a27 | |||
| 8da68871f0 | |||
| 0149b34006 | |||
| 22a8b2614b | |||
| 1f7df79f1b | |||
| 84f3cb5ac1 | |||
| 6718a28c87 | |||
| 8d56ee8182 | |||
| 9788acb08e | |||
| 9a2e48b1da | |||
| d77d4a8f19 | |||
| c4b106b9a2 | |||
| 1a5e483c21 | |||
| 1355ea4b60 | |||
| 6c7f06e240 | |||
| a87c76bea6 | |||
| 84ce4ea325 | |||
| 31b8312877 | |||
| 496b09d93f | |||
| 7ea1f305af | |||
| cab08fbffe | |||
| efed015f3a | |||
| ba6fea8ddb | |||
| 45dc925f8b |
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.31.1
|
||||
uses: crate-ci/typos@v1.32.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: deploy-registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
# Matches release/<namespace>/<resource_name>/<semantic_version>
|
||||
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
|
||||
- "release/*/*/v*.*.*"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Set id-token permission for gcloud
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193
|
||||
with:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
|
||||
- name: Deploy to registry.coder.com
|
||||
run: |
|
||||
gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --branch main
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,268 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="katman_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 841.9 595.3">
|
||||
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #2e3c4e;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.st9 {
|
||||
fill: #7300e5;
|
||||
}
|
||||
|
||||
.st10, .st11 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st12 {
|
||||
fill: url(#Adsız_degrade_4);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st13 {
|
||||
fill: url(#Adsız_degrade_41);
|
||||
}
|
||||
|
||||
.st14 {
|
||||
fill: #d7c8f9;
|
||||
}
|
||||
|
||||
.st15 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.st16 {
|
||||
stroke: #d1d1d6;
|
||||
stroke-width: .5px;
|
||||
}
|
||||
|
||||
.st16, .st17, .st11 {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st17 {
|
||||
stroke: #fff;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st7 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st18 {
|
||||
fill: url(#Adsız_degrade_5);
|
||||
}
|
||||
|
||||
.st8 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st19 {
|
||||
fill: url(#Adsız_degrade_2);
|
||||
}
|
||||
</style>
|
||||
<mask id="mask" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1058_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st14" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_2" data-name="Adsız degrade 2" x1="68.8" y1="563" x2="-1.8" y2="563.4" gradientTransform="translate(194.4 765.6) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".7"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-1" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1038_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_4" data-name="Adsız degrade 4" x1="69.7" y1="567.5" x2="-11.4" y2="566.9" gradientTransform="translate(178.6 1012.1) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".6"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-2" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1018_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st9" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_41" data-name="Adsız degrade 4" x1="21.7" y1="568.3" x2="163.7" y2="568.3" gradientTransform="translate(551.4 969.2) scale(1 -1)" xlink:href="#Adsız_degrade_4"/>
|
||||
<linearGradient id="Adsız_degrade_5" data-name="Adsız degrade 5" x1="56.8" y1="647.8" x2="-1.1" y2="536.3" gradientTransform="translate(349.3 2027.2) scale(3 -3)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2fabff"/>
|
||||
<stop offset=".3" stop-color="#5570ff"/>
|
||||
<stop offset=".6" stop-color="#7b36ff"/>
|
||||
<stop offset=".8" stop-color="#6a2cdc"/>
|
||||
<stop offset="1" stop-color="#5921b8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect class="st15" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st2">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st4">
|
||||
<path class="st17" d="M424.4,139.4c-2.9-1.6-7.6-1.6-10.5,0l-129.6,69.3c-2.9,1.6-5.3,5.5-5.3,8.8v153.3c0,3.3,2.4,7.3,5.2,8.9l129.6,71.9c2.9,1.6,7.6,1.6,10.5,0l130.7-71.6c2.9-1.6,5.3-5.6,5.3-8.9v-150.9c0-3.3-2.4-7.3-5.3-8.9l-130.7-72Z"/>
|
||||
</g>
|
||||
<g class="st8">
|
||||
<path class="st17" d="M424.4,157.4c-2.9-1.6-7.7-1.6-10.6,0l-115.9,61.1c-2.9,1.5-5.3,5.5-5.3,8.8v135.9c0,3.3,2.4,7.3,5.2,8.9l116,64.5c2.9,1.6,7.6,1.6,10.5,0l117.1-63.3c2.9-1.6,5.3-5.5,5.3-8.9v-135c0-3.3-2.4-7.3-5.3-8.8l-117.1-63.2Z"/>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st19" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M236.1,209.6c.2,0,.3-.1.4-.2,0,0,0-.3,0-.5v-1.3c0-.3-.1-.5-.3-.5s-.2,0-.4,0c-.4,0-.8.2-1.3.2-.5,0-.9,0-1.2,0-1.4,0-2.4-.4-3.1-1.1s-1-1.8-1-3.3v-.5c0-1.5.3-2.6,1-3.4s1.7-1.1,3.1-1.1,1.4,0,2.2.3c.2,0,.4,0,.4,0,.2,0,.3-.1.3-.5v-1.3c0-.2,0-.4,0-.5s-.2-.2-.3-.2c-1-.3-1.9-.4-2.9-.4-2.2,0-3.9.6-5.1,1.9-1.2,1.3-1.8,3.1-1.8,5.4s.6,4.1,1.7,5.3c1.2,1.2,2.9,1.8,5.1,1.8s2.2-.1,3.2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M240.4,204.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM246.2,208.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M252.3,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M267.7,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M272.5,203.6c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM278.3,209.6c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M283.6,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M299.1,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st12" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
<path class="st16" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M213.9,439.7h1.7c2.4,0,3.6,1.5,3.6,4.4v.4c0,2.9-1.2,4.4-3.6,4.4h-1.7v-9.2ZM215.9,451.2c2,0,3.6-.6,4.7-1.8,1.1-1.2,1.7-2.9,1.7-5.1s-.6-3.9-1.7-5.1c-1.1-1.2-2.7-1.8-4.8-1.8h-4.5c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v12.9c0,.2,0,.3.1.4,0,0,.2.1.4.1h4.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M227.4,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM229.1,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M241.5,451.1c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M246.6,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM248.3,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M263,450.8c.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1,.1-1.5.3c-.5.2-1,.5-1.4.8v-4.9c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.5c.3,0,.5-.2.6-.4v-.6c.5.4.9.7,1.5.9.5.2,1.1.3,1.7.3s1.6-.2,2.2-.7ZM257.9,448.7v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M269.8,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM271.5,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M284.1,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M289.3,445c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM295.1,451c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M303.8,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st13" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M549.5,407.8c.2,0,.4,0,.5-.1,0,0,.2-.2.2-.4l.8-2.6h4.8l.8,2.6c0,.2.2.3.2.4.1,0,.2.1.4.1h2.2c.3,0,.4-.1.4-.3s0-.1,0-.2c0,0,0-.2-.1-.3l-4.4-12.5c0-.2-.1-.3-.2-.4,0,0-.3-.1-.5-.1h-2.2c-.2,0-.3,0-.3,0,0,0-.1,0-.2.2,0,0-.1.2-.2.3l-4.4,12.5c0,.1,0,.2-.1.3,0,.1,0,.2,0,.2,0,.2.1.3.4.3h2.1ZM551.7,402.5l1.7-5.8,1.8,5.8h-3.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M564.9,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM564.4,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M577.2,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM576.7,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M590.6,407.8c.2,0,.3-.1.4-.2,0,0,0-.2,0-.5v-1c0-.2,0-.3,0-.3,0,0-.1-.1-.3-.1s-.1,0-.2,0c0,0-.2,0-.3,0-.5,0-.7-.3-.7-1v-11.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v11.4c0,2,.9,3,2.7,3s1,0,1.4-.2Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M595.8,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM595.5,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M604.6,408c.4,0,.8-.2,1.2-.3.2,0,.3-.1.3-.2,0,0,0-.2,0-.4v-1c0-.3-.1-.4-.3-.4s-.2,0-.3,0c-.6.2-1.1.2-1.6.2-.9,0-1.6-.2-2-.7-.4-.5-.6-1.2-.6-2.2v-.3c0-1,.2-1.8.7-2.2.4-.5,1.1-.7,2.1-.7s1,0,1.6.2c0,0,0,0,.1,0,0,0,0,0,.1,0,.2,0,.3-.2.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.4-.2-.7-.3-1.5-.4-2.3-.4-1.6,0-2.9.5-3.8,1.4-.9,1-1.4,2.3-1.4,4s.4,3,1.3,3.9c.9.9,2.1,1.4,3.7,1.4s.8,0,1.2-.1Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M610.7,405.9c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM612.4,407.8c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M624.7,407.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M630.1,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM629.9,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M635.7,402.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM641.5,406.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M647.6,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M663.3,407.3c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st18" d="M452.1,74c-19.6-11.3-43.8-11.3-63.4,0l-145.3,83.9c-19.6,11.3-31.7,32.3-31.7,54.9v167.8c0,22.7,12.1,43.6,31.7,54.9l145.3,83.9c19.6,11.3,43.8,11.3,63.4,0l145.3-83.9c19.6-11.3,31.7-32.3,31.7-54.9v-167.8c0-22.7-12.1-43.6-31.7-54.9l-145.3-83.9Z"/>
|
||||
<path class="st10" d="M438.6,293.4l-12.7,25.4,120.5,69.8,12.7-25.4-120.5-69.8ZM422.7,269.8c-2-1.1-4.4-1.1-6.3,0l-21.1,12.2c-2,1.1-3.2,3.2-3.2,5.5v24.4c0,2.3,1.2,4.4,3.2,5.5l21.1,12.2c2,1.1,4.4,1.1,6.3,0l21.1-12.2c2-1.1,3.2-3.2,3.2-5.5v-24.4c0-2.3-1.2-4.4-3.2-5.5l-21.1-12.2ZM411.6,163.4c7.9-4.5,17.5-4.5,25.4,0l98.2,56.7c7.9,4.5,12.7,12.9,12.7,22v113.4c0,9.1-4.8,17.4-12.7,22l-98.2,56.7c-7.9,4.5-17.5,4.5-25.4,0l-98.2-56.7c-7.9-4.5-12.7-12.9-12.7-22v-113.4c0-9.1,4.8-17.4,12.7-22l98.2-56.7ZM395.7,135.2l-103.1,59.5c-15.7,9.1-25.4,25.8-25.4,44v119c0,18.1,9.7,34.9,25.4,44l103.1,59.5c15.7,9.1,35,9.1,50.8,0l103.1-59.5c15.7-9.1,25.4-25.8,25.4-44v-119c0-18.1-9.7-34.9-25.4-44l-103.1-59.5c-15.7-9.1-35-9.1-50.8,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -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 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_devcontainer</title><circle cx="16" cy="16" r="14" style="fill:#193e63"/><polygon points="10.777 22.742 9.343 21.348 12.729 17.865 9.346 14.417 10.774 13.017 15.525 17.859 10.777 22.742" style="fill:#add1ea"/><polygon points="21.42 19.101 22.854 17.706 19.468 14.224 22.851 10.776 21.423 9.376 16.672 14.218 21.42 19.101" style="fill:#add1ea"/></svg>
|
||||
|
After Width: | Height: | Size: 575 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M897.246 286.869H889.819C850.735 286.808 819.017 318.46 819.017 357.539V515.589C819.017 547.15 792.93 572.716 761.882 572.716C743.436 572.716 725.02 563.433 714.093 547.85L552.673 317.304C539.28 298.16 517.486 286.747 493.895 286.747C457.094 286.747 423.976 318.034 423.976 356.657V515.619C423.976 547.181 398.103 572.746 366.842 572.746C348.335 572.746 329.949 563.463 319.021 547.881L138.395 289.882C134.316 284.038 125.154 286.93 125.154 294.052V431.892C125.154 438.862 127.285 445.619 131.272 451.34L309.037 705.2C319.539 720.204 335.033 731.344 352.9 735.392C397.616 745.557 438.77 711.135 438.77 667.278V508.406C438.77 476.845 464.339 451.279 495.904 451.279H495.995C515.02 451.279 532.857 460.562 543.785 476.145L705.235 706.661C718.659 725.835 739.327 737.218 763.983 737.218C801.606 737.218 833.841 705.9 833.841 667.308V508.376C833.841 476.815 859.41 451.249 890.975 451.249H897.276C901.233 451.249 904.43 448.053 904.43 444.097V294.021C904.43 290.065 901.233 286.869 897.276 286.869H897.246Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,3 @@
|
||||
# Code of Conduct
|
||||
|
||||
[Please see our code of conduct on the official Coder website](https://coder.com/docs/contributing/CODE_OF_CONDUCT)
|
||||
@@ -5,7 +5,7 @@
|
||||
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
|
||||
|
||||
- [Bun](https://bun.sh/) – Used to run tests for each module/template to validate overall functionality and correctness of Terraform output
|
||||
- [Go](https://go.dev/) – Used to validate all README files in the directory
|
||||
- [Go](https://go.dev/) – Used to validate all README files in the directory. The README content is used to populate [the Registry website](https://registry.coder.com).
|
||||
|
||||
### Installing Bun
|
||||
|
||||
@@ -49,35 +49,87 @@ Once Go has been installed, verify the installation via:
|
||||
go version
|
||||
```
|
||||
|
||||
### Adding a new module/template (coming soon)
|
||||
## Namespaces
|
||||
|
||||
Once Bun (and possibly Go) have been installed, clone this repository. From there, you can run this script to make it easier to start contributing a new module or template:
|
||||
All Coder resources are scoped to namespaces placed at the top level of the `/registry` directory. Any modules or templates must be placed inside a namespace to be accepted as a contribution. For example, all modules created by CoderEmployeeBob would be placed under `/registry/coderemployeebob/modules`, with a subdirectory for each individual module the user has published.
|
||||
|
||||
If a namespace is already taken, you will need to create a different, unique namespace, but will still be able to choose any display name. (The display name is shown in the Registry website. More info below.)
|
||||
|
||||
### Namespace (contributor profile) README files
|
||||
|
||||
More information about contributor profile README files can be found below.
|
||||
|
||||
### Images
|
||||
|
||||
Any images needed for either the main namespace directory or a module/template can be placed in a relative `/images` directory at the top of the namespace directory. (e.g., CoderEmployeeBob can have a `/registry/coderemployeebob/images` directory, that can be referenced by the main README file, as well as a README file in `/registry/coderemployeebob/modules/custom_module/README.md`.) This is to minimize the risk of file name conflicts between different users as they add images to help illustrate parts of their README files.
|
||||
|
||||
## Coder modules
|
||||
|
||||
### Adding a new module
|
||||
|
||||
> [!WARNING]
|
||||
> These instructions cannot be followed just yet; the script referenced will be made available shortly. Contributors looking to add modules early will need to create all directories manually.
|
||||
|
||||
Once Bun (and possibly Go) have been installed, clone the Coder Registry repository. From there, you can run this script to make it easier to start contributing a new module or template:
|
||||
|
||||
```shell
|
||||
./new.sh NAME_OF_NEW_MODULE
|
||||
./new.sh USER_NAMESPACE/NAME_OF_NEW_MODULE
|
||||
```
|
||||
|
||||
You can also create the correct module/template files manually.
|
||||
You can also create a module file manually by creating the necessary files and directories.
|
||||
|
||||
## Testing a Module
|
||||
### The composition of a Coder module
|
||||
|
||||
Each Coder Module must contain the following files:
|
||||
|
||||
- A `main.tf` file that defines the main Terraform-based functionality
|
||||
- A `main.test.ts` file that is used to validate that the module works as expected
|
||||
- A `README.md` file containing required information (listed below)
|
||||
|
||||
You are free to include any additional files in the module, as needed by the module. For example, the [Windows RDP module](https://github.com/coder/registry/tree/main/registry/coder/modules/windows-rdp) contains additional files for injecting specific functionality into a Coder Workspace.
|
||||
|
||||
> [!NOTE]
|
||||
> Some legacy modules do not have test files defined just yet. This will be addressed soon.
|
||||
|
||||
### The `main.tf` file
|
||||
|
||||
This file defines all core Terraform functionality, to be mixed into your Coder workspaces. More information about [Coder's use of Terraform can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), and [general information about the Terraform language can be found in the official documentation](https://developer.hashicorp.com/terraform/docs).
|
||||
|
||||
### The structure of a module README
|
||||
|
||||
Validation criteria for module README files is listed below.
|
||||
|
||||
### Testing a Module
|
||||
|
||||
> [!IMPORTANT]
|
||||
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It falls to the author to test the module locally before submitting a PR.
|
||||
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It is expected the author has tested the module locally before opening a PR. Feel free to reference existing test files to get an idea for how to set them up.
|
||||
|
||||
All general-purpose test helpers for validating Terraform can be found in the top-level `/testing` directory. The helpers run `terraform apply` on modules that use variables, testing the script output against containers.
|
||||
|
||||
When writing a test file, you can import the test utilities via the `~test` import alias:
|
||||
|
||||
```ts
|
||||
// This works regardless of how deeply-nested your test file is in the file
|
||||
// structure
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The testing suite must be able to run docker containers with the `--network=host` flag. This 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.
|
||||
> The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS or 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.
|
||||
|
||||
You can reference the existing `*.test.ts` files to get an idea for how to set up tests.
|
||||
#### Running tests
|
||||
|
||||
You can run all tests by running this command:
|
||||
You can run all tests by running this command from the root of the Registry directory:
|
||||
|
||||
```shell
|
||||
bun test
|
||||
```
|
||||
|
||||
Note that tests can take some time to run, so you probably don't want to be running this as part of your development loop.
|
||||
Note that running _all_ tests can take some time, so you likely don't want to be running this command as part of your core development loop.
|
||||
|
||||
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
|
||||
|
||||
@@ -94,9 +146,9 @@ module "example" {
|
||||
}
|
||||
```
|
||||
|
||||
## Adding/modifying README files
|
||||
## Updating README files
|
||||
|
||||
This repo uses Go to do a quick validation of each README. If you are working with the README files at all, it is strongly recommended that you install Go, so that the files can be validated locally.
|
||||
This repo uses Go to validate each README file. If you are working with the README files at all (i.e., creating them, modifying them), it is strongly recommended that you install Go (installation instructions mentioned above), so that the files can be validated locally.
|
||||
|
||||
### Validating all README files
|
||||
|
||||
@@ -106,7 +158,7 @@ To validate all README files throughout the entire repo, you can run the followi
|
||||
go build ./cmd/readmevalidation && ./readmevalidation
|
||||
```
|
||||
|
||||
The resulting binary is already part of the `.gitignore` file, but you can quickly remove it with:
|
||||
The resulting binary is already part of the `.gitignore` file, but you can remove it with:
|
||||
|
||||
```shell
|
||||
rm ./readmevalidation
|
||||
@@ -114,17 +166,17 @@ rm ./readmevalidation
|
||||
|
||||
### README validation criteria
|
||||
|
||||
The following criteria exists for one of two reasons: (1) content accessibility, or (2) having content be designed in a way that's easy for the Registry site build step to use:
|
||||
The following criteria exists for two reasons:
|
||||
|
||||
1. Content accessibility
|
||||
2. Having content be designed in a way that's easy for the Registry site build step to use
|
||||
|
||||
#### General README requirements
|
||||
|
||||
- There must be a frontmatter section.
|
||||
- There must be exactly one h1 header, and it must be at the very top
|
||||
- There must be exactly one h1 header, and it must be at the very top, directly below the frontmatter.
|
||||
- The README body (if it exists) must start with an h1 header. No other content (including GitHub-Flavored Markdown alerts) is allowed to be placed above it.
|
||||
- When increasing the level of a header, the header's level must be incremented by one each time.
|
||||
- Additional image/video assets can be placed in one of two places:
|
||||
- In the same user namespace directory where that user's main content lives
|
||||
- In the top-level `.icons` directory
|
||||
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
|
||||
|
||||
```txt
|
||||
@@ -133,31 +185,64 @@ The following criteria exists for one of two reasons: (1) content accessibility,
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
#### Contributor profiles
|
||||
#### Namespace (contributor profile) criteria
|
||||
|
||||
- The README body is allowed to be empty, but if it isn't, it must follow all the rules above.
|
||||
- The frontmatter supports the following fields:
|
||||
- `display_name` (required string) – The name to use when displaying your user profile in the Coder Registry site
|
||||
- `bio` (optional string) – A short description of who you are
|
||||
- `github` (required string) – Your GitHub handle
|
||||
- `avatar_url` (optional string) – A relative/absolute URL pointing to your avatar
|
||||
- `linkedin` (optional string) – A URL pointing to your LinkedIn page
|
||||
- `support_email` (optional string) – An email for users to reach you at if they need help with a published module/template
|
||||
- `employer_github` (optional string) – The name of another user namespace whom you'd like to have associated with your account. The namespace must also exist in the repo, or else the README validation will fail.
|
||||
- `status` (optional string union) – If defined, must be one of "community", "partner", or "official". "Community" is treated as the default value if not specified, and should be used for the majority of external contributions. "Official" should be used for Coder and Coder satellite companies. "Partner" is for companies who have a formal business agreement with Coder.
|
||||
In addition to the general criteria, all README files must have the following:
|
||||
|
||||
#### Modules and templates
|
||||
- Frontmatter metadata with support for the following fields:
|
||||
|
||||
- The frontmatter supports the following fields:
|
||||
- `description` (required string) A short description of what the module/template does.
|
||||
- `icon` (required string) – A URL pointing to the icon to use for the module/template when viewing it on the Registry website.
|
||||
- `display_name` (optional string) – A name to display instead of the name intuited from the module's/template's directory name
|
||||
- `verified` (optional boolean) – A boolean indicated that the Coder team has officially tested and vouched for the functionality/reliability of a given module or template. This field should only be changed by Coder employees.
|
||||
- `tags` (optional string array) – A list of tags to associate with the module/template. Users will be able to search for these tags from the Registry website.
|
||||
- `display_name` (required string) – The name to use when displaying your user profile in the Coder Registry site.
|
||||
- `bio` (optional string) – A short description of who you are.
|
||||
- `github` (optional string) – Your GitHub handle.
|
||||
- `avatar_url` (optional string) – A relative/absolute URL pointing to your avatar for the Registry site. It is strongly recommended that you commit avatar images to this repo and reference them via a relative URL.
|
||||
- `linkedin` (optional string) – A URL pointing to your LinkedIn page.
|
||||
- `support_email` (optional string) – An email for users to reach you at if they need help with a published module/template.
|
||||
- `status` (string union) – If defined, this must be one of `"community"`, `"partner"`, or `"official"`. `"community"` should be used for the majority of external contributions. `"partner"` is for companies who have a formal business partnership with Coder. `"official"` should be used only by Coder employees.
|
||||
|
||||
- The README body (the content that goes directly below the frontmatter) is allowed to be empty, but if it isn't, it must follow all the rules above.
|
||||
|
||||
You are free to customize the body of a contributor profile however you like, adding any number of images or information. Its content will never be rendered in the Registry website.
|
||||
|
||||
Additional information can be placed in the README file below the content listed above, using any number of headers.
|
||||
|
||||
Additional image/video assets can be placed in the same user namespace directory where that user's main content lives.
|
||||
|
||||
#### Module criteria
|
||||
|
||||
In addition to the general criteria, all README files must have the following:
|
||||
|
||||
- Frontmatter that describes metadata for the module:
|
||||
- `display_name` (required string) – This is the name displayed on the Coder Registry website
|
||||
- `description` (required string) – A short description of the module, which is displayed on the Registry website
|
||||
- `icon` (required string) – A relative/absolute URL pointing to the icon to display for the module in the Coder Registry website.
|
||||
- `verified` (optional boolean) – Indicates whether the module has been officially verified by Coder. Please do not set this without approval from a Coder employee.
|
||||
- `tags` (required string array) – A list of metadata tags to describe the module. Used in the Registry site for search and navigation functionality.
|
||||
- `maintainer_github` (deprecated string) – The name of the creator of the module. This field exists for backwards compatibility with previous versions of the Registry, but going forward, the value will be inferred from the namespace directory.
|
||||
- `partner_github` (deprecated string) - The name of any additional creators for a module. This field exists for backwards compatibility with previous versions of the Registry, but should not ever be used going forward.
|
||||
- The following content directly under the h1 header (without another header between them):
|
||||
|
||||
- A description of what the module does
|
||||
- A Terraform snippet for letting other users import the functionality
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
Additional information can be placed in the README file below the content listed above, using any number of headers.
|
||||
|
||||
Additional image/video assets can be placed in one of two places:
|
||||
|
||||
1. In the same user namespace directory where that user's main content lives
|
||||
2. If the image is an icon, it can be placed in the top-level `.icons` directory (this is done because a lot of modules will be based off the same products)
|
||||
|
||||
## Releases
|
||||
|
||||
The release process is automated with these steps:
|
||||
The release process involves the following steps:
|
||||
|
||||
### 1. Create and merge a new PR
|
||||
|
||||
@@ -168,33 +253,42 @@ The release process is automated with these steps:
|
||||
|
||||
After merging to `main`, a maintainer will:
|
||||
|
||||
- View all modules and their current versions:
|
||||
- Check out the merge commit:
|
||||
|
||||
```shell
|
||||
./release.sh --list
|
||||
git checkout MERGE_COMMIT_ID
|
||||
```
|
||||
|
||||
- Determine the next version number based on changes:
|
||||
|
||||
- **Patch version** (1.2.3 → 1.2.4): Bug fixes
|
||||
- **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs
|
||||
- **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types)
|
||||
|
||||
- Create and push an annotated tag:
|
||||
- Create annotated tags for each module that was changed:
|
||||
|
||||
```shell
|
||||
# Fetch latest changes
|
||||
git fetch origin
|
||||
|
||||
# Create and push tag
|
||||
./release.sh module-name 1.2.3 --push
|
||||
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
|
||||
```
|
||||
|
||||
The tag format will be: `release/module-name/v1.2.3`
|
||||
- Push the tags to origin:
|
||||
|
||||
```shell
|
||||
git push origin release/$namespace/$module/v$version
|
||||
```
|
||||
|
||||
For example, to release version 1.0.14 of the coder/aider module:
|
||||
|
||||
```shell
|
||||
git tag -a "release/coder/aider/v1.0.14" -m "Release coder/aider v1.0.14"
|
||||
git push origin release/coder/aider/v1.0.14
|
||||
```
|
||||
|
||||
### Version Numbers
|
||||
|
||||
Version numbers should follow semantic versioning:
|
||||
|
||||
- **Patch version** (1.2.3 → 1.2.4): Bug fixes
|
||||
- **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs
|
||||
- **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types)
|
||||
|
||||
### 3. Publishing to Coder Registry
|
||||
|
||||
Our automated processes will handle publishing new data to [registry.coder.com](https://registry.coder.com).
|
||||
After tags are pushed, the changes will be published to [registry.coder.com](https://registry.coder.com).
|
||||
|
||||
> [!NOTE]
|
||||
> Some data in registry.coder.com is fetched on demand from the [coder/modules](https://github.com/coder/modules) repo's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
|
||||
> Some data in registry.coder.com is fetched on demand from this repository's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
# Coder Registry
|
||||
|
||||
Publish Coder modules and templates for other developers to use.
|
||||
[Registry Site](https://registry.coder.com) • [Coder OSS](https://github.com/coder/coder) • [Coder Docs](https://www.coder.com/docs) • [Official Discord](https://discord.gg/coder)
|
||||
|
||||
[](https://github.com/coder/registry/actions/workflows/check_registry_site_health.yaml)
|
||||
|
||||
Coder Registry is a community-driven platform for extending your Coder workspaces. Publish reusable Terraform as Coder Modules for users all over the world.
|
||||
|
||||
> [!NOTE]
|
||||
> The Coder Registry repo will be updated to support Coder Templates in the coming weeks. You can currently find all official templates in the official coder/coder repo, [under the `examples/templates` directory](https://github.com/coder/coder/tree/main/examples/templates).
|
||||
|
||||
## Overview
|
||||
|
||||
Coder is built on HashiCorp's open-source Terraform language to provide developers an easy, declarative way to define the infrastructure for their remote development environments. Coder-flavored versions of Terraform allow you to mix in reusable Terraform snippets to add integrations with other popular development tools, such as JetBrains, Cursor, or Visual Studio Code.
|
||||
|
||||
Simply add the correct import snippet, along with any data dependencies, and your workspace can start using the new functionality immediately.
|
||||
|
||||

|
||||
|
||||
More information [about Coder Modules can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), while more information [about Coder Templates can be found here](https://coder.com/docs/admin/templates/creating-templates).
|
||||
|
||||
## Getting started
|
||||
|
||||
The easiest way to discover new modules and templates is by visiting [the official Coder Registry website](https://registry.coder.com/). The website is a full mirror of the Coder Registry repo, and it is where .tar versions of the various resources can be downloaded from, for use within your Coder deployment.
|
||||
|
||||
Note that while Coder has a baseline set of requirements for allowing an external PR to be published, Coder cannot vouch for the validity or functionality of a resource until that resource has been flagged with the `verified` status. [All modules under the Coder namespace](https://github.com/coder/registry/tree/main/registry/coder) are automatically verified.
|
||||
|
||||
### Getting started with modules
|
||||
|
||||
To get started with a module, navigate to that module's page in either the registry site, or the main repo:
|
||||
|
||||
- [The Cursor repo directory](https://github.com/coder/registry/tree/main/registry/coder/modules/cursor)
|
||||
- [The Cursor module page on the main website](https://registry.coder.com/modules/cursor)
|
||||
|
||||
In both cases, the main README contains a Terraform snippet for integrating the module into your workspace. The snippet for Cursor looks like this:
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
Simply include that snippet inside your Coder template, defining any data dependencies referenced, and the next time you create a new workspace, the functionality will be ready for you to use.
|
||||
|
||||
## Contributing
|
||||
|
||||
We are always accepting new contributions. [Please see our contributing guide for more information.](./CONTRIBUTING.md)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Security
|
||||
|
||||
[Please see our security policy on the official Coder website](https://coder.com/security/policy)
|
||||
@@ -0,0 +1,354 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var supportedResourceTypes = []string{"modules", "templates"}
|
||||
|
||||
type coderResourceFrontmatter struct {
|
||||
Description string `yaml:"description"`
|
||||
IconURL string `yaml:"icon"`
|
||||
DisplayName *string `yaml:"display_name"`
|
||||
Verified *bool `yaml:"verified"`
|
||||
Tags []string `yaml:"tags"`
|
||||
}
|
||||
|
||||
// coderResourceReadme represents a README describing a Terraform resource used
|
||||
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both
|
||||
// Coder Modules and Coder Templates
|
||||
type coderResourceReadme struct {
|
||||
resourceType string
|
||||
filePath string
|
||||
body string
|
||||
frontmatter coderResourceFrontmatter
|
||||
}
|
||||
|
||||
func validateCoderResourceDisplayName(displayName *string) error {
|
||||
if displayName != nil && *displayName == "" {
|
||||
return errors.New("if defined, display_name must not be empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceDescription(description string) error {
|
||||
if description == "" {
|
||||
return errors.New("frontmatter description cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceIconURL(iconURL string) []error {
|
||||
problems := []error{}
|
||||
|
||||
if iconURL == "" {
|
||||
problems = append(problems, errors.New("icon URL cannot be empty"))
|
||||
return problems
|
||||
}
|
||||
|
||||
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
|
||||
if isAbsoluteURL {
|
||||
if _, err := url.ParseRequestURI(iconURL); err != nil {
|
||||
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
|
||||
}
|
||||
if strings.Contains(iconURL, "?") {
|
||||
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Would normally be skittish about having relative paths like this, but it
|
||||
// should be safe because we have guarantees about the structure of the
|
||||
// repo, and where this logic will run
|
||||
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
|
||||
strings.HasPrefix(iconURL, "/") ||
|
||||
strings.HasPrefix(iconURL, "../../../../.icons")
|
||||
if !isPermittedRelativeURL {
|
||||
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
func validateCoderResourceTags(tags []string) error {
|
||||
if tags == nil {
|
||||
return errors.New("provided tags array is nil")
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// All of these tags are used for the module/template filter controls in the
|
||||
// Registry site. Need to make sure they can all be placed in the browser
|
||||
// URL without issue
|
||||
invalidTags := []string{}
|
||||
for _, t := range tags {
|
||||
if t != url.QueryEscape(t) {
|
||||
invalidTags = append(invalidTags, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(invalidTags) != 0 {
|
||||
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Todo: This is a holdover from the validation logic used by the Coder Modules
|
||||
// repo. It gives us some assurance, but realistically, we probably want to
|
||||
// parse any Terraform code snippets, and make some deeper guarantees about how
|
||||
// it's structured. Just validating whether it *can* be parsed as Terraform
|
||||
// would be a big improvement.
|
||||
var terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=")
|
||||
|
||||
func validateCoderResourceReadmeBody(body string) []error {
|
||||
trimmed := strings.TrimSpace(body)
|
||||
var errs []error
|
||||
errs = append(errs, validateReadmeBody(trimmed)...)
|
||||
|
||||
foundParagraph := false
|
||||
terraformCodeBlockCount := 0
|
||||
foundTerraformVersionRef := false
|
||||
|
||||
lineNum := 0
|
||||
isInsideCodeBlock := false
|
||||
isInsideTerraform := false
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||
for lineScanner.Scan() {
|
||||
lineNum++
|
||||
nextLine := lineScanner.Text()
|
||||
|
||||
// Code assumes that invalid headers would've already been handled by
|
||||
// the base validation function, so we don't need to check deeper if the
|
||||
// first line isn't an h1
|
||||
if lineNum == 1 {
|
||||
if !strings.HasPrefix(nextLine, "# ") {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(nextLine, "```") {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
|
||||
if isInsideTerraform {
|
||||
terraformCodeBlockCount++
|
||||
}
|
||||
if strings.HasPrefix(nextLine, "```hcl") {
|
||||
errs = append(errs, errors.New("all .hcl language references must be converted to .tf"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if isInsideCodeBlock {
|
||||
if isInsideTerraform {
|
||||
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Code assumes that we can treat this case as the end of the "h1
|
||||
// section" and don't need to process any further lines
|
||||
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
|
||||
break
|
||||
}
|
||||
|
||||
// Code assumes that if we've reached this point, the only other options
|
||||
// are: (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset
|
||||
// references made via [] syntax
|
||||
trimmedLine := strings.TrimSpace(nextLine)
|
||||
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
|
||||
foundParagraph = foundParagraph || isParagraph
|
||||
}
|
||||
|
||||
if terraformCodeBlockCount == 0 {
|
||||
errs = append(errs, errors.New("did not find Terraform code block within h1 section"))
|
||||
} else {
|
||||
if terraformCodeBlockCount > 1 {
|
||||
errs = append(errs, errors.New("cannot have more than one Terraform code block in h1 section"))
|
||||
}
|
||||
if !foundTerraformVersionRef {
|
||||
errs = append(errs, errors.New("did not find Terraform code block that specifies 'version' field"))
|
||||
}
|
||||
}
|
||||
if !foundParagraph {
|
||||
errs = append(errs, errors.New("did not find paragraph within h1 section"))
|
||||
}
|
||||
if isInsideCodeBlock {
|
||||
errs = append(errs, errors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateCoderResourceReadme(rm coderResourceReadme) []error {
|
||||
var errs []error
|
||||
|
||||
for _, err := range validateCoderResourceReadmeBody(rm.body) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
|
||||
if err := validateCoderResourceDisplayName(rm.frontmatter.DisplayName); err != nil {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if err := validateCoderResourceDescription(rm.frontmatter.Description); err != nil {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if err := validateCoderResourceTags(rm.frontmatter.Tags); err != nil {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
|
||||
for _, err := range validateCoderResourceIconURL(rm.frontmatter.IconURL) {
|
||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, error) {
|
||||
fm, body, err := separateFrontmatter(rm.rawText)
|
||||
if err != nil {
|
||||
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := coderResourceFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return coderResourceReadme{
|
||||
resourceType: resourceType,
|
||||
filePath: rm.filePath,
|
||||
body: body,
|
||||
frontmatter: yml,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[string]coderResourceReadme, error) {
|
||||
resources := map[string]coderResourceReadme{}
|
||||
var yamlParsingErrs []error
|
||||
for _, rm := range rms {
|
||||
p, err := parseCoderResourceReadme(resourceType, rm)
|
||||
if err != nil {
|
||||
yamlParsingErrs = append(yamlParsingErrs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
resources[p.filePath] = p
|
||||
}
|
||||
if len(yamlParsingErrs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
errors: yamlParsingErrs,
|
||||
}
|
||||
}
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, readme := range resources {
|
||||
errors := validateCoderResourceReadme(readme)
|
||||
if len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
}
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
errors: yamlValidationErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// Todo: Need to beef up this function by grabbing each image/video URL from
|
||||
// the body's AST
|
||||
func validateCoderResourceRelativeUrls(resources map[string]coderResourceReadme) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
registryFiles, err := os.ReadDir(rootRegistryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allReadmeFiles []readme
|
||||
var errs []error
|
||||
for _, rf := range registryFiles {
|
||||
if !rf.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceRootPath := path.Join(rootRegistryPath, rf.Name(), resourceType)
|
||||
resourceDirs, err := os.ReadDir(resourceRootPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rd := range resourceDirs {
|
||||
if !rd.IsDir() || rd.Name() == ".coder" {
|
||||
continue
|
||||
}
|
||||
|
||||
resourceReadmePath := path.Join(resourceRootPath, rd.Name(), "README.md")
|
||||
rm, err := os.ReadFile(resourceReadmePath)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
allReadmeFiles = append(allReadmeFiles, readme{
|
||||
filePath: resourceReadmePath,
|
||||
rawText: string(rm),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseFileLoad,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
return allReadmeFiles, nil
|
||||
}
|
||||
|
||||
func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||
return fmt.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
||||
}
|
||||
|
||||
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Processing %d README files\n", len(allReadmeFiles))
|
||||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType)
|
||||
|
||||
err = validateCoderResourceRelativeUrls(resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed testSamples/sampleReadmeBody.md
|
||||
var testBody string
|
||||
|
||||
func TestValidateCoderResourceReadmeBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("Parses a valid README body with zero issues", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errs := validateCoderResourceReadmeBody(testBody)
|
||||
for _, e := range errs {
|
||||
t.Error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -16,18 +16,18 @@ import (
|
||||
var validContributorStatuses = []string{"official", "partner", "community"}
|
||||
|
||||
type contributorProfileFrontmatter struct {
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Bio string `yaml:"bio"`
|
||||
// Script assumes that if value is nil, the Registry site build step will
|
||||
// backfill the value with the user's GitHub avatar URL
|
||||
AvatarURL *string `yaml:"avatar"`
|
||||
LinkedinURL *string `yaml:"linkedin"`
|
||||
WebsiteURL *string `yaml:"website"`
|
||||
SupportEmail *string `yaml:"support_email"`
|
||||
ContributorStatus *string `yaml:"status"`
|
||||
DisplayName string `yaml:"display_name"`
|
||||
Bio string `yaml:"bio"`
|
||||
ContributorStatus string `yaml:"status"`
|
||||
// Script assumes that if avatar URL is nil, the Registry site build step
|
||||
// will backfill the value with the user's GitHub avatar URL
|
||||
AvatarURL *string `yaml:"avatar"`
|
||||
LinkedinURL *string `yaml:"linkedin"`
|
||||
WebsiteURL *string `yaml:"website"`
|
||||
SupportEmail *string `yaml:"support_email"`
|
||||
}
|
||||
|
||||
type contributorProfile struct {
|
||||
type contributorProfileReadme struct {
|
||||
frontmatter contributorProfileFrontmatter
|
||||
namespace string
|
||||
filePath string
|
||||
@@ -105,13 +105,9 @@ func validateContributorWebsite(websiteURL *string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateContributorStatus(status *string) error {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !slices.Contains(validContributorStatuses, *status) {
|
||||
return fmt.Errorf("contributor status %q is not valid", *status)
|
||||
func validateContributorStatus(status string) error {
|
||||
if !slices.Contains(validContributorStatuses, status) {
|
||||
return fmt.Errorf("contributor status %q is not valid", status)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -155,52 +151,52 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateContributorYaml(yml contributorProfile) []error {
|
||||
func validateContributorReadme(rm contributorProfileReadme) []error {
|
||||
allErrs := []error{}
|
||||
|
||||
if err := validateContributorDisplayName(yml.frontmatter.DisplayName); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if err := validateContributorLinkedinURL(yml.frontmatter.LinkedinURL); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
if err := validateContributorLinkedinURL(rm.frontmatter.LinkedinURL); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if err := validateContributorWebsite(yml.frontmatter.WebsiteURL); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
if err := validateContributorWebsite(rm.frontmatter.WebsiteURL); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
if err := validateContributorStatus(yml.frontmatter.ContributorStatus); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
if err := validateContributorStatus(rm.frontmatter.ContributorStatus); err != nil {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
|
||||
for _, err := range validateContributorSupportEmail(yml.frontmatter.SupportEmail) {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
for _, err := range validateContributorSupportEmail(rm.frontmatter.SupportEmail) {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
for _, err := range validateContributorAvatarURL(yml.frontmatter.AvatarURL) {
|
||||
allErrs = append(allErrs, addFilePathToError(yml.filePath, err))
|
||||
for _, err := range validateContributorAvatarURL(rm.frontmatter.AvatarURL) {
|
||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func parseContributorProfile(rm readme) (contributorProfile, error) {
|
||||
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
|
||||
fm, _, err := separateFrontmatter(rm.rawText)
|
||||
if err != nil {
|
||||
return contributorProfile{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := contributorProfileFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return contributorProfile{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return contributorProfile{
|
||||
return contributorProfileReadme{
|
||||
filePath: rm.filePath,
|
||||
frontmatter: yml,
|
||||
namespace: strings.TrimSuffix(strings.TrimPrefix(rm.filePath, "registry/"), "/README.md"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfile, error) {
|
||||
profilesByNamespace := map[string]contributorProfile{}
|
||||
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
|
||||
profilesByNamespace := map[string]contributorProfileReadme{}
|
||||
yamlParsingErrors := []error{}
|
||||
for _, rm := range readmeEntries {
|
||||
p, err := parseContributorProfile(rm)
|
||||
@@ -224,7 +220,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, p := range profilesByNamespace {
|
||||
errors := validateContributorYaml(p)
|
||||
errors := validateContributorReadme(p)
|
||||
if len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
continue
|
||||
@@ -276,7 +272,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
||||
return allReadmeFiles, nil
|
||||
}
|
||||
|
||||
func validateContributorRelativeUrls(contributors map[string]contributorProfile) error {
|
||||
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error {
|
||||
// This function only validates relative avatar URLs for now, but it can be
|
||||
// beefed up to validate more in the future
|
||||
errs := []error{}
|
||||
|
||||
@@ -14,7 +14,7 @@ type validationPhaseError struct {
|
||||
var _ error = validationPhaseError{}
|
||||
|
||||
func (vpe validationPhaseError) Error() string {
|
||||
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase.String())
|
||||
msg := fmt.Sprintf("Error during %q phase of README validation:", vpe.phase)
|
||||
for _, e := range vpe.errors {
|
||||
msg += fmt.Sprintf("\n- %v", e)
|
||||
}
|
||||
|
||||
@@ -23,13 +23,18 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
var errs []error
|
||||
err := validateAllContributorFiles()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
err = validateAllCoderResourceFilesOfType("modules")
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
for _, err := range errs {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -31,9 +32,8 @@ func separateFrontmatter(readmeText string) (string, string, error) {
|
||||
fm := ""
|
||||
body := ""
|
||||
fenceCount := 0
|
||||
lineScanner := bufio.NewScanner(
|
||||
strings.NewReader(strings.TrimSpace(readmeText)),
|
||||
)
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
|
||||
for lineScanner.Scan() {
|
||||
nextLine := lineScanner.Text()
|
||||
if fenceCount < 2 && nextLine == fence {
|
||||
@@ -66,48 +66,113 @@ func separateFrontmatter(readmeText string) (string, string, error) {
|
||||
return fm, strings.TrimSpace(body), nil
|
||||
}
|
||||
|
||||
var readmeHeaderRe = regexp.MustCompile("^(#{1,})(\\s*)")
|
||||
|
||||
// Todo: This seems to work okay for now, but the really proper way of doing
|
||||
// this is by parsing this as an AST, and then checking the resulting nodes
|
||||
func validateReadmeBody(body string) []error {
|
||||
trimmed := strings.TrimSpace(body)
|
||||
|
||||
if trimmed == "" {
|
||||
return []error{errors.New("README body is empty")}
|
||||
}
|
||||
|
||||
// If the very first line of the README, there's a risk that the rest of the
|
||||
// validation logic will break, since we don't have many guarantees about
|
||||
// how the README is actually structured
|
||||
if !strings.HasPrefix(trimmed, "# ") {
|
||||
return []error{errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
latestHeaderLevel := 0
|
||||
foundFirstH1 := false
|
||||
isInCodeBlock := false
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||
for lineScanner.Scan() {
|
||||
nextLine := lineScanner.Text()
|
||||
|
||||
// Have to check this because a lot of programming languages support #
|
||||
// comments (including Terraform), and without any context, there's no
|
||||
// way to tell the difference between a markdown header and code comment
|
||||
if strings.HasPrefix(nextLine, "```") {
|
||||
isInCodeBlock = !isInCodeBlock
|
||||
continue
|
||||
}
|
||||
if isInCodeBlock {
|
||||
continue
|
||||
}
|
||||
|
||||
headerGroups := readmeHeaderRe.FindStringSubmatch(nextLine)
|
||||
if headerGroups == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
spaceAfterHeader := headerGroups[2]
|
||||
if spaceAfterHeader == "" {
|
||||
errs = append(errs, errors.New("header does not have space between header characters and main header text"))
|
||||
}
|
||||
|
||||
nextHeaderLevel := len(headerGroups[1])
|
||||
if nextHeaderLevel == 1 && !foundFirstH1 {
|
||||
foundFirstH1 = true
|
||||
latestHeaderLevel = 1
|
||||
continue
|
||||
}
|
||||
|
||||
// If we have obviously invalid headers, it's not really safe to keep
|
||||
// proceeding with the rest of the content
|
||||
if nextHeaderLevel == 1 {
|
||||
errs = append(errs, errors.New("READMEs cannot contain more than h1 header"))
|
||||
break
|
||||
}
|
||||
if nextHeaderLevel > 6 {
|
||||
errs = append(errs, fmt.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
|
||||
break
|
||||
}
|
||||
|
||||
// This is something we need to enforce for accessibility, not just for
|
||||
// the Registry website, but also when users are viewing the README
|
||||
// files in the GitHub web view
|
||||
if nextHeaderLevel > latestHeaderLevel && nextHeaderLevel != (latestHeaderLevel+1) {
|
||||
errs = append(errs, fmt.Errorf("headers are not allowed to increase more than 1 level at a time"))
|
||||
continue
|
||||
}
|
||||
|
||||
// As long as the above condition passes, there's no problems with
|
||||
// going up a header level or going down 1+ header levels
|
||||
latestHeaderLevel = nextHeaderLevel
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// validationPhase represents a specific phase during README validation. It is
|
||||
// expected that each phase is discrete, and errors during one will prevent a
|
||||
// future phase from starting.
|
||||
type validationPhase int
|
||||
type validationPhase string
|
||||
|
||||
const (
|
||||
// validationPhaseFileStructureValidation indicates when the entire Registry
|
||||
// directory is being verified for having all files be placed in the file
|
||||
// system as expected.
|
||||
validationPhaseFileStructureValidation validationPhase = iota
|
||||
validationPhaseFileStructureValidation validationPhase = "File structure validation"
|
||||
|
||||
// validationPhaseFileLoad indicates when README files are being read from
|
||||
// the file system
|
||||
validationPhaseFileLoad
|
||||
validationPhaseFileLoad = "Filesystem reading"
|
||||
|
||||
// validationPhaseReadmeParsing indicates when a README's frontmatter is
|
||||
// being parsed as YAML. This phase does not include YAML validation.
|
||||
validationPhaseReadmeParsing
|
||||
validationPhaseReadmeParsing = "README parsing"
|
||||
|
||||
// validationPhaseReadmeValidation indicates when a README's frontmatter is
|
||||
// being validated as proper YAML with expected keys.
|
||||
validationPhaseReadmeValidation
|
||||
validationPhaseReadmeValidation = "README validation"
|
||||
|
||||
// validationPhaseAssetCrossReference indicates when a README's frontmatter
|
||||
// is having all its relative URLs be validated for whether they point to
|
||||
// valid resources.
|
||||
validationPhaseAssetCrossReference
|
||||
validationPhaseAssetCrossReference = "Cross-referencing relative asset URLs"
|
||||
)
|
||||
|
||||
func (p validationPhase) String() string {
|
||||
switch p {
|
||||
case validationPhaseFileStructureValidation:
|
||||
return "File structure validation"
|
||||
case validationPhaseFileLoad:
|
||||
return "Filesystem reading"
|
||||
case validationPhaseReadmeParsing:
|
||||
return "README parsing"
|
||||
case validationPhaseReadmeValidation:
|
||||
return "README validation"
|
||||
case validationPhaseAssetCrossReference:
|
||||
return "Cross-referencing relative asset URLs"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown validation phase: %d", p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
supportedResourceTypes = []string{"modules", "templates"}
|
||||
supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
|
||||
)
|
||||
var supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
|
||||
|
||||
func validateCoderResourceSubdirectory(dirPath string) []error {
|
||||
errs := []error{}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type terraformLifecycleCondition struct {
|
||||
// Any terraform expression that evaluates to a boolean
|
||||
expression string
|
||||
errorMessage *string
|
||||
}
|
||||
|
||||
type terraformLifecycle struct {
|
||||
createBeforeDestroy bool
|
||||
preventDestroy bool
|
||||
ignoreChanges []string
|
||||
replaceTriggeredBy []string
|
||||
preCondition *terraformLifecycleCondition
|
||||
postCondition *terraformLifecycleCondition
|
||||
}
|
||||
|
||||
type terraformForEachKind string
|
||||
|
||||
const (
|
||||
terraformForEachKindMap terraformForEachKind = "map"
|
||||
terraformForEachKindSet terraformForEachKind = "set"
|
||||
)
|
||||
|
||||
type terraformForEach struct {
|
||||
kind terraformForEachKind
|
||||
// If the kind is "map", all values should be guaranteed to be a definite
|
||||
// string for each key. If it's "set", all values should be nil
|
||||
values map[string]*string
|
||||
}
|
||||
|
||||
type terraformVariableKind string
|
||||
|
||||
const (
|
||||
terraformVariableKindString terraformVariableKind = "string"
|
||||
terraformVariableKindNumber terraformVariableKind = "number"
|
||||
terraformVariableKindBool terraformVariableKind = "bool"
|
||||
terraformVariableKindList terraformVariableKind = "list"
|
||||
terraformVariableKindMap terraformVariableKind = "map"
|
||||
terraformVariableKindSet terraformVariableKind = "set"
|
||||
// This value kind is incredibly rare. It corresponds to any variable with
|
||||
// a null value but no defined type
|
||||
terraformVariableKindUnknown terraformVariableKind = "unknown"
|
||||
)
|
||||
|
||||
type terraformVariable struct {
|
||||
kind terraformVariableKind
|
||||
value any
|
||||
}
|
||||
|
||||
// coderTerraformModule represents the values that can be parsed from a
|
||||
// Terraform module that are relevant to a Coder deployment. Most of the fields
|
||||
// are derived from the main Terraform spec, but some additional Coder-specific
|
||||
// fields are appended, too, for convenience
|
||||
type coderTerraformModule struct {
|
||||
// Corresponds to the optional `lifecycle` metadata field available to all
|
||||
// resource types
|
||||
Lifecycle *terraformLifecycle `json:"lifecycle"`
|
||||
// Corresponds to the optional `for_each` metadata field available to all
|
||||
// resource types
|
||||
ForEach *terraformForEach `json:"for_each"`
|
||||
// Corresponds to the `source` field in a module block
|
||||
ModuleSource string `json:"module_source"`
|
||||
// Corresponds to the `version` field in Terraform module blocks. Note that
|
||||
// while the Terraform spec marks this field as optional, Coder requires
|
||||
// that one always be defined.
|
||||
Version string `json:"version"`
|
||||
// Corresponds to the optional `provider` field in a module block
|
||||
Provider *string `json:"provider"`
|
||||
// Corresponds to optional `depends_on` field for module blocks
|
||||
DependsOn []string `json:"depends_on"`
|
||||
// Corresponds to the `count` field for any Terraform resource type. It
|
||||
// defines the number of resource instances to create when using Terraform
|
||||
// Apply.
|
||||
InstanceCount int `json:"instance_count"`
|
||||
// Corresponds to `agent_id`` field in a module block. Terraform doesn't
|
||||
// have any built-in concept of an agent_id, but it's needed to make a
|
||||
// module work with a Coder deployment
|
||||
AgentID string `json:"agent_id"`
|
||||
// Captures all other arbitrary values defined for a Terraform module block.
|
||||
// Note that while Terraform itself has you define all other fields at the
|
||||
// same level as the well-known/official fields, they've been isolated into
|
||||
// a map for the Go struct definition to improve type-safety
|
||||
Values map[string]terraformVariable `json:"values"`
|
||||
// The raw Terraform snippet used to derive the coderTerraformModule struct
|
||||
SourceCode string `json:"source_code"`
|
||||
}
|
||||
|
||||
var _ json.Marshaler = &coderTerraformModule{}
|
||||
|
||||
func (ctmCopy coderTerraformModule) MarshalJSON() ([]byte, error) {
|
||||
if ctmCopy.Lifecycle != nil {
|
||||
lCopy := &terraformLifecycle{
|
||||
createBeforeDestroy: ctmCopy.Lifecycle.createBeforeDestroy,
|
||||
preventDestroy: ctmCopy.Lifecycle.preventDestroy,
|
||||
ignoreChanges: ctmCopy.Lifecycle.ignoreChanges,
|
||||
replaceTriggeredBy: ctmCopy.Lifecycle.replaceTriggeredBy,
|
||||
preCondition: ctmCopy.Lifecycle.preCondition,
|
||||
postCondition: ctmCopy.Lifecycle.postCondition,
|
||||
}
|
||||
// Make sure that both slices always get serialized as JSON null if
|
||||
// they're empty. Serializing as an empty JSON array has no extra
|
||||
// semantics
|
||||
if len(lCopy.ignoreChanges) == 0 {
|
||||
lCopy.ignoreChanges = nil
|
||||
}
|
||||
if len(lCopy.replaceTriggeredBy) == 0 {
|
||||
lCopy.replaceTriggeredBy = nil
|
||||
}
|
||||
ctmCopy.Lifecycle = lCopy
|
||||
}
|
||||
|
||||
if ctmCopy.ForEach != nil {
|
||||
feCopy := &terraformForEach{
|
||||
kind: ctmCopy.ForEach.kind,
|
||||
values: ctmCopy.ForEach.values,
|
||||
}
|
||||
// Make sure that the values map is NEVER serialized as JSON null
|
||||
if feCopy.values == nil {
|
||||
feCopy.values = make(map[string]*string)
|
||||
}
|
||||
ctmCopy.ForEach = feCopy
|
||||
}
|
||||
|
||||
if len(ctmCopy.DependsOn) == 0 {
|
||||
ctmCopy.DependsOn = nil
|
||||
}
|
||||
|
||||
if ctmCopy.Values == nil {
|
||||
ctmCopy.Values = make(map[string]terraformVariable)
|
||||
}
|
||||
for k, tfValue := range ctmCopy.Values {
|
||||
switch tfValue.kind {
|
||||
case terraformVariableKindString, terraformVariableKindBool, terraformVariableKindNumber, terraformVariableKindUnknown:
|
||||
continue
|
||||
case terraformVariableKindSet, terraformVariableKindList:
|
||||
recast, ok := tfValue.value.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as set/list", k, tfValue.kind)
|
||||
}
|
||||
if recast == nil {
|
||||
ctmCopy.Values[k] = terraformVariable{
|
||||
kind: tfValue.kind,
|
||||
value: []any{},
|
||||
}
|
||||
}
|
||||
case terraformVariableKindMap:
|
||||
recast, ok := tfValue.value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as map", k, tfValue.kind)
|
||||
}
|
||||
if recast == nil {
|
||||
ctmCopy.Values[k] = terraformVariable{
|
||||
kind: tfValue.kind,
|
||||
value: make(map[string]any),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(ctmCopy)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Goose
|
||||
|
||||
Run the [Goose](https://block.github.io/goose/) agent in your workspace to generate code and perform tasks.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `screen` must be installed in your workspace to run Goose in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
|
||||
## Examples
|
||||
|
||||
Your workspace must have `screen` installed to use this.
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "The Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Goose"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
# Set the prompt and system prompt for Goose via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
GOOSE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help write code.
|
||||
|
||||
Run all long running tasks (e.g. npm run dev) in the background and not in the foreground.
|
||||
|
||||
Periodically check in on background tasks.
|
||||
|
||||
Notify Coder of the status of the task before and after your steps.
|
||||
EOT
|
||||
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# An API key is required for experiment_auto_configure
|
||||
# See https://block.github.io/goose/docs/getting-started/providers
|
||||
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
}
|
||||
}
|
||||
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
|
||||
# Run Goose in the background
|
||||
experiment_use_screen = true
|
||||
|
||||
# Avoid configuring Goose manually
|
||||
experiment_auto_configure = true
|
||||
|
||||
# Required for experiment_auto_configure
|
||||
experiment_goose_provider = "anthropic"
|
||||
experiment_goose_model = "claude-3-5-sonnet-latest"
|
||||
}
|
||||
```
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
# Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL
|
||||
icon = "https://raw.githubusercontent.com/block/goose/refs/heads/main/ui/desktop/src/images/icon.svg"
|
||||
}
|
||||
```
|
||||
@@ -14,7 +14,7 @@ tags: [helper]
|
||||
```tf
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.2"
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
```tf
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
@@ -48,7 +48,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
```tf
|
||||
module "MODULE_NAME" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
@@ -64,7 +64,7 @@ Run code-server in the background, don't fetch it from GitHub:
|
||||
|
||||
```tf
|
||||
module "MODULE_NAME" {
|
||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
|
||||
|
After Width: | Height: | Size: 53 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "modules",
|
||||
"name": "registry",
|
||||
"scripts": {
|
||||
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
|
||||
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
@@ -4,7 +4,6 @@ bio: Coder provisions cloud development environments via Terraform, supporting L
|
||||
github: coder
|
||||
linkedin: https://www.linkedin.com/company/coderhq
|
||||
website: https://www.coder.com
|
||||
support_email: support@coder.com
|
||||
status: official
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
display_name: Aider
|
||||
description: Run Aider AI pair programming in your workspace
|
||||
icon: ../../../../.icons/aider.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, ai, aider]
|
||||
---
|
||||
|
||||
# Aider
|
||||
|
||||
Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux.
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace
|
||||
- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter
|
||||
- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background
|
||||
- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding
|
||||
- **Project Integration**: Works with any project directory, including Git repositories
|
||||
- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal
|
||||
- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable
|
||||
|
||||
## Module Parameters
|
||||
|
||||
| Parameter | Description | Type | Default |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------- | -------- | ------------------- |
|
||||
| `agent_id` | The ID of a Coder agent (required) | `string` | - |
|
||||
| `folder` | The folder to run Aider in | `string` | `/home/coder` |
|
||||
| `install_aider` | Whether to install Aider | `bool` | `true` |
|
||||
| `aider_version` | The version of Aider to install | `string` | `"latest"` |
|
||||
| `use_screen` | Whether to use screen for running Aider in the background | `bool` | `true` |
|
||||
| `use_tmux` | Whether to use tmux instead of screen for running Aider in the background | `bool` | `false` |
|
||||
| `session_name` | Name for the persistent session (screen or tmux) | `string` | `"aider"` |
|
||||
| `order` | Position of the app in the UI presentation | `number` | `null` |
|
||||
| `icon` | The icon to use for the app | `string` | `"/icon/aider.svg"` |
|
||||
| `experiment_report_tasks` | Whether to enable task reporting | `bool` | `true` |
|
||||
| `system_prompt` | System prompt for instructing Aider on task reporting and behavior | `string` | See default in code |
|
||||
| `task_prompt` | Task prompt to use with Aider | `string` | `""` |
|
||||
| `ai_provider` | AI provider to use with Aider (openai, anthropic, azure, etc.) | `string` | `"anthropic"` |
|
||||
| `ai_model` | AI model to use (can use Aider's built-in aliases like "sonnet", "4o") | `string` | `"sonnet"` |
|
||||
| `ai_api_key` | API key for the selected AI provider | `string` | `""` |
|
||||
| `custom_env_var_name` | Custom environment variable name when using custom provider | `string` | `""` |
|
||||
| `experiment_pre_install_script` | Custom script to run before installing Aider | `string` | `null` |
|
||||
| `experiment_post_install_script` | Custom script to run after installing Aider | `string` | `null` |
|
||||
| `experiment_additional_extensions` | Additional extensions configuration in YAML format to append to the config | `string` | `null` |
|
||||
|
||||
> **Note**: `use_screen` and `use_tmux` cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic setup with API key
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
This basic setup will:
|
||||
|
||||
- Install Aider in the workspace
|
||||
- Create a persistent screen session named "aider"
|
||||
- Configure Aider to use Anthropic Claude 3.7 Sonnet model
|
||||
- Enable task reporting (configures Aider to report tasks to Coder MCP)
|
||||
|
||||
### Using OpenAI with tmux
|
||||
|
||||
```tf
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
use_tmux = true
|
||||
ai_provider = "openai"
|
||||
ai_model = "4o" # Uses Aider's built-in alias for gpt-4o
|
||||
ai_api_key = var.openai_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Using a custom provider
|
||||
|
||||
```tf
|
||||
variable "custom_api_key" {
|
||||
type = string
|
||||
description = "Custom provider API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_provider = "custom"
|
||||
custom_env_var_name = "MY_CUSTOM_API_KEY"
|
||||
ai_model = "custom-model"
|
||||
ai_api_key = var.custom_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (Experimental)
|
||||
|
||||
You can extend Aider's capabilities by adding custom extensions:
|
||||
|
||||
```tf
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
pip install some-custom-dependency
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
custom-extension:
|
||||
args: []
|
||||
cmd: custom-extension-command
|
||||
description: A custom extension for Aider
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: custom-extension
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Task Reporting (Experimental)
|
||||
|
||||
> This functionality is in early access as of Coder v2.21 and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents)
|
||||
>
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
Task reporting is **enabled by default** in this module, allowing you to:
|
||||
|
||||
- Send an initial prompt to Aider during workspace creation
|
||||
- Monitor task progress in the Coder UI
|
||||
- Use the `coder_parameter` resource to collect prompts from users
|
||||
|
||||
### Setting up Task Reporting
|
||||
|
||||
To use task reporting effectively:
|
||||
|
||||
1. Add the Coder Login module to your template
|
||||
2. Configure the necessary variables to pass the task prompt
|
||||
3. Optionally add a coder_parameter to collect prompts from users
|
||||
|
||||
Here's a complete example:
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Aider"
|
||||
mutable = true
|
||||
ephemeral = true
|
||||
}
|
||||
|
||||
module "aider" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/aider/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# Optionally customize the system prompt
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
When a task prompt is provided via the `task_prompt` variable, the module automatically:
|
||||
|
||||
1. Combines the system prompt with the task prompt into a single message in the format:
|
||||
|
||||
```
|
||||
SYSTEM PROMPT:
|
||||
[system_prompt content]
|
||||
|
||||
This is your current task: [task_prompt]
|
||||
```
|
||||
|
||||
2. Executes the task during workspace creation using the `--message` and `--yes-always` flags
|
||||
3. Logs task output to `$HOME/.aider.log` for reference
|
||||
|
||||
If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration.
|
||||
|
||||
## Using Aider in Your Workspace
|
||||
|
||||
After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation.
|
||||
|
||||
### Session Options
|
||||
|
||||
You can run Aider in three different ways:
|
||||
|
||||
1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button
|
||||
|
||||
- Simple setup without persistent context
|
||||
- Suitable for quick coding sessions
|
||||
|
||||
2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections
|
||||
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
|
||||
3. **Tmux Mode**: Run Aider in a tmux session instead of screen
|
||||
|
||||
- Set `use_tmux = true` to enable
|
||||
- Session name: "aider" (or configured via `session_name`)
|
||||
- Configures tmux with mouse support for shared sessions
|
||||
|
||||
Persistent sessions (screen/tmux) allow you to:
|
||||
|
||||
- Disconnect and reconnect without losing context
|
||||
- Run Aider in the background while doing other work
|
||||
- Switch between terminal and browser interfaces
|
||||
|
||||
### Available AI Providers and Models
|
||||
|
||||
Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases:
|
||||
|
||||
| Provider | Example Models/Aliases | Default Model |
|
||||
| ------------- | --------------------------------------------- | ---------------------- |
|
||||
| **anthropic** | "sonnet" (Claude 3.7 Sonnet), "opus", "haiku" | "sonnet" |
|
||||
| **openai** | "4o" (GPT-4o), "4" (GPT-4), "3.5-turbo" | "4o" |
|
||||
| **azure** | Azure OpenAI models | "gpt-4" |
|
||||
| **google** | "gemini" (Gemini Pro), "gemini-2.5-pro" | "gemini-2.5-pro" |
|
||||
| **cohere** | "command-r-plus", etc. | "command-r-plus" |
|
||||
| **mistral** | "mistral-large-latest" | "mistral-large-latest" |
|
||||
| **ollama** | "llama3", etc. | "llama3" |
|
||||
| **custom** | Any model name with custom ENV variable | - |
|
||||
|
||||
For a complete and up-to-date list of supported aliases and models, please refer to the [Aider LLM documentation](https://aider.chat/docs/llms.html) and the [Aider LLM Leaderboards](https://aider.chat/docs/leaderboards.html) which show performance comparisons across different models.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions`
|
||||
2. **API key issues**: Ensure you've entered the correct API key for your selected provider
|
||||
3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace
|
||||
|
||||
For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/).
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("aider", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("configures task prompt correctly", async () => {
|
||||
const testPrompt = "Add a hello world function";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
task_prompt: testPrompt,
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
`This is your current task: ${testPrompt}`,
|
||||
);
|
||||
expect(instance.script).toContain("aider --architect --yes-always");
|
||||
});
|
||||
|
||||
it("handles custom system prompt", async () => {
|
||||
const customPrompt = "Report all tasks with state: working";
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
system_prompt: customPrompt,
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(customPrompt);
|
||||
});
|
||||
|
||||
it("handles pre and post install scripts", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
experiment_pre_install_script: "echo 'Pre-install script executed'",
|
||||
experiment_post_install_script: "echo 'Post-install script executed'",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain("Running pre-install script");
|
||||
expect(instance.script).toContain("Running post-install script");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh");
|
||||
expect(instance.script).toContain("base64 -d > /tmp/post_install.sh");
|
||||
});
|
||||
|
||||
it("validates that use_screen and use_tmux cannot both be true", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
use_screen: true,
|
||||
use_tmux: true,
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(instance.script).toContain(
|
||||
"Error: Both use_screen and use_tmux cannot be enabled at the same time",
|
||||
);
|
||||
expect(instance.script).toContain("exit 1");
|
||||
});
|
||||
|
||||
it("configures Aider with known provider and model", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "anthropic",
|
||||
ai_model: "sonnet",
|
||||
ai_api_key: "test-anthropic-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model sonnet");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using anthropic provider and model: sonnet",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles custom provider with custom env var and API key", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
ai_provider: "custom",
|
||||
custom_env_var_name: "MY_CUSTOM_API_KEY",
|
||||
ai_model: "custom-model",
|
||||
ai_api_key: "test-custom-key",
|
||||
});
|
||||
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
expect(instance.script).toContain(
|
||||
'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"',
|
||||
);
|
||||
expect(instance.script).toContain("--model custom-model");
|
||||
expect(instance.script).toContain(
|
||||
"Starting Aider using custom provider and model: custom-model",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,502 @@
|
||||
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."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/aider.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Aider in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "install_aider" {
|
||||
type = bool
|
||||
description = "Whether to install Aider."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "aider_version" {
|
||||
type = string
|
||||
description = "The version of Aider to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Aider in the background"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Aider in the background"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "session_name" {
|
||||
type = string
|
||||
description = "Name for the persistent session (screen or tmux)"
|
||||
default = "aider"
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for instructing Aider on task reporting and behavior"
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt to use with Aider"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Aider."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "ai_provider" {
|
||||
type = string
|
||||
description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)"
|
||||
default = "anthropic"
|
||||
validation {
|
||||
condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider)
|
||||
error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom"
|
||||
}
|
||||
}
|
||||
|
||||
variable "ai_model" {
|
||||
type = string
|
||||
description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc."
|
||||
default = "sonnet"
|
||||
}
|
||||
|
||||
variable "ai_api_key" {
|
||||
type = string
|
||||
description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "custom_env_var_name" {
|
||||
type = string
|
||||
description = "Custom environment variable name when using custom provider"
|
||||
default = ""
|
||||
}
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: aider
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
|
||||
# Combine system prompt and task prompt for aider
|
||||
combined_prompt = trimspace(<<-EOT
|
||||
SYSTEM PROMPT:
|
||||
${var.system_prompt}
|
||||
|
||||
This is your current task: ${var.task_prompt}
|
||||
EOT
|
||||
)
|
||||
|
||||
# Map providers to their environment variable names
|
||||
provider_env_vars = {
|
||||
openai = "OPENAI_API_KEY"
|
||||
anthropic = "ANTHROPIC_API_KEY"
|
||||
azure = "AZURE_OPENAI_API_KEY"
|
||||
google = "GOOGLE_API_KEY"
|
||||
cohere = "COHERE_API_KEY"
|
||||
mistral = "MISTRAL_API_KEY"
|
||||
ollama = "OLLAMA_HOST"
|
||||
custom = var.custom_env_var_name
|
||||
}
|
||||
|
||||
# Get the environment variable name for selected provider
|
||||
env_var_name = local.provider_env_vars[var.ai_provider]
|
||||
|
||||
# Model flag for aider command
|
||||
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
|
||||
}
|
||||
|
||||
# Install and Initialize Aider
|
||||
resource "coder_script" "aider" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
echo "Setting up Aider AI pair programming..."
|
||||
|
||||
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
|
||||
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${var.folder}"
|
||||
|
||||
if [ "$(uname)" = "Linux" ]; then
|
||||
echo "Checking dependencies for Linux..."
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if ! command_exists tmux; then
|
||||
echo "Installing tmux for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq tmux
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q tmux
|
||||
else
|
||||
dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "tmux is already installed, skipping installation."
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! command_exists screen; then
|
||||
echo "Installing screen for persistent sessions..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq screen
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q screen
|
||||
else
|
||||
dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "screen is already installed, skipping installation."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "This module currently only supports Linux workspaces."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_aider}" = "true" ]; then
|
||||
echo "Installing Aider..."
|
||||
|
||||
if ! command_exists python3 || ! command_exists pip3; then
|
||||
echo "Installing Python dependencies required for Aider..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq python3-pip python3-venv
|
||||
else
|
||||
apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges"
|
||||
apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q python3-pip python3-virtualenv
|
||||
else
|
||||
dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges"
|
||||
fi
|
||||
else
|
||||
echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found."
|
||||
fi
|
||||
else
|
||||
echo "Python is already installed, skipping installation."
|
||||
fi
|
||||
|
||||
if ! command_exists aider; then
|
||||
curl -LsSf https://aider.chat/install.sh | sh
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Aider to report tasks via Coder MCP..."
|
||||
|
||||
mkdir -p "$HOME/.config/aider"
|
||||
|
||||
cat > "$HOME/.config/aider/config.yml" << EOL
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
echo "Added Coder MCP extension to Aider config.yml"
|
||||
fi
|
||||
|
||||
echo "Starting persistent Aider session..."
|
||||
|
||||
touch "$HOME/.aider.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in tmux session..."
|
||||
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
|
||||
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
# Configure tmux for shared sessions
|
||||
if [ ! -f "$HOME/.tmux.conf" ]; then
|
||||
echo "Creating ~/.tmux.conf with shared session settings..."
|
||||
echo "set -g mouse on" > "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
|
||||
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
|
||||
echo "set -g mouse on" >> "$HOME/.tmux.conf"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
|
||||
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
else
|
||||
if [ -n "${var.task_prompt}" ]; then
|
||||
echo "Running Aider with message in screen session..."
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
|
||||
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
|
||||
else
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..."
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..."
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
|
||||
screen -U -dmS ${var.session_name} bash -c "
|
||||
cd ${var.folder}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Aider setup complete!"
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
# Aider CLI app
|
||||
resource "coder_app" "aider_cli" {
|
||||
agent_id = var.agent_id
|
||||
slug = "aider"
|
||||
display_name = "Aider"
|
||||
icon = var.icon
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t ${var.session_name} 2>/dev/null; then
|
||||
echo "Attaching to existing Aider tmux session..."
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting new Aider tmux session..."
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
echo "Error: No existing Aider session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
screen -xRR ${var.session_name}
|
||||
else
|
||||
cd "${var.folder}"
|
||||
echo "Starting Aider directly..."
|
||||
export ${local.env_var_name}="${var.ai_api_key}"
|
||||
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
|
||||
fi
|
||||
EOT
|
||||
order = var.order
|
||||
}
|
||||
@@ -18,7 +18,7 @@ Enable DCV Server and Web Client on Windows workspaces.
|
||||
```tf
|
||||
module "dcv" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/amazon-dcv-windows/coder"
|
||||
source = "registry.coder.com/coder/amazon-dcv-windows/coder"
|
||||
version = "1.0.24"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
display_name: Amazon Q
|
||||
description: Run Amazon Q in your workspace to access Amazon's AI coding assistant.
|
||||
icon: ../../../../.icons/amazon-q.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, ai, aws, amazon-q]
|
||||
---
|
||||
|
||||
# Amazon Q
|
||||
|
||||
Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's AI coding assistant. This module installs and launches Amazon Q, with support for background operation, task reporting, and custom pre/post install scripts.
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
# Required: see below for how to generate
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must generate an authenticated Amazon Q tarball on another machine:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
Paste the result into the `experiment_auth_tarball` variable.
|
||||
- To run in the background, your workspace must have `screen` or `tmux` installed.
|
||||
|
||||
<details>
|
||||
<summary><strong>How to generate the Amazon Q auth tarball (step-by-step)</strong></summary>
|
||||
|
||||
**1. Install and authenticate Amazon Q on your local machine:**
|
||||
|
||||
- Download and install Amazon Q from the [official site](https://aws.amazon.com/q/developer/).
|
||||
- Run `q login` and complete the authentication process in your terminal.
|
||||
|
||||
**2. Locate your Amazon Q config directory:**
|
||||
|
||||
- The config is typically stored at `~/.local/share/amazon-q`.
|
||||
|
||||
**3. Generate the tarball:**
|
||||
|
||||
- Run the following command in your terminal:
|
||||
```sh
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
**4. Copy the output:**
|
||||
|
||||
- The command will output a long string. Copy this entire string.
|
||||
|
||||
**5. Paste into your Terraform variable:**
|
||||
|
||||
- Assign the string to the `experiment_auth_tarball` variable in your Terraform configuration, for example:
|
||||
```tf
|
||||
variable "amazon_q_auth_tarball" {
|
||||
type = string
|
||||
default = "PASTE_LONG_STRING_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
**Note:**
|
||||
|
||||
- You must re-generate the tarball if you log out or re-authenticate Amazon Q on your local machine.
|
||||
- This process is required for each user who wants to use Amazon Q in their workspace.
|
||||
|
||||
[Reference: Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/generate-docs.html)
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
### Run Amazon Q in the background with tmux
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_use_tmux = true
|
||||
}
|
||||
```
|
||||
|
||||
### Enable task reporting (experimental)
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
|
||||
### Run custom scripts before/after install
|
||||
|
||||
```tf
|
||||
module "amazon-q" {
|
||||
source = "registry.coder.com/coder/amazon-q/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_pre_install_script = "echo Pre-install!"
|
||||
experiment_post_install_script = "echo Post-install!"
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Name | Required | Default | Description |
|
||||
| -------------------------------- | -------- | ------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `agent_id` | Yes | — | The ID of a Coder agent. |
|
||||
| `experiment_auth_tarball` | Yes | — | Base64-encoded, zstd-compressed tarball of a pre-authenticated Amazon Q config directory. |
|
||||
| `install_amazon_q` | No | `true` | Whether to install Amazon Q. |
|
||||
| `amazon_q_version` | No | `latest` | Version to install. |
|
||||
| `experiment_use_screen` | No | `false` | Use GNU screen for background operation. |
|
||||
| `experiment_use_tmux` | No | `false` | Use tmux for background operation. |
|
||||
| `experiment_report_tasks` | No | `false` | Enable task reporting to Coder. |
|
||||
| `experiment_pre_install_script` | No | `null` | Custom script to run before install. |
|
||||
| `experiment_post_install_script` | No | `null` | Custom script to run after install. |
|
||||
| `icon` | No | `/icon/amazon-q.svg` | The icon to use for the app. |
|
||||
| `folder` | No | `/home/coder` | The folder to run Amazon Q in. |
|
||||
| `order` | No | `null` | The order determines the position of app in the UI presentation. |
|
||||
| `system_prompt` | No | See [main.tf](./main.tf) | The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting. |
|
||||
| `ai_prompt` | No | See [main.tf](./main.tf) | The initial task prompt to send to Amazon Q. |
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
|
||||
- If neither is set, Amazon Q runs in the foreground.
|
||||
- For more details, see the [main.tf](./main.tf) source.
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
|
||||
const moduleDir = path.resolve(__dirname);
|
||||
|
||||
const requiredVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
};
|
||||
|
||||
describe("amazon-q module", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// 1. Required variables
|
||||
testRequiredVariables(moduleDir, requiredVars);
|
||||
|
||||
// 2. coder_script resource is created
|
||||
it("creates coder_script resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const scriptResource = findResourceInstance(state, "coder_script");
|
||||
expect(scriptResource).toBeDefined();
|
||||
expect(scriptResource.agent_id).toBe(requiredVars.agent_id);
|
||||
// Optionally, check that the script contains expected lines
|
||||
expect(scriptResource.script).toContain("Installing Amazon Q");
|
||||
});
|
||||
|
||||
// 3. coder_app resource is created
|
||||
it("creates coder_app resource", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
const appResource = findResourceInstance(state, "coder_app", "amazon_q");
|
||||
expect(appResource).toBeDefined();
|
||||
expect(appResource.agent_id).toBe(requiredVars.agent_id);
|
||||
});
|
||||
|
||||
// Add more state-based tests as needed
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
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."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/amazon-q.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Amazon Q in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "install_amazon_q" {
|
||||
type = bool
|
||||
description = "Whether to install Amazon Q."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "amazon_q_version" {
|
||||
type = string
|
||||
description = "The version of Amazon Q to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "experiment_use_screen" {
|
||||
type = bool
|
||||
description = "Whether to use screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Amazon Q in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Amazon Q."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory. After running `q login` on another machine, you may generate it with: `cd ~/.local/share/amazon-q && tar -c . | zstd | base64 -w 0`"
|
||||
default = "tarball"
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting."
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks, you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message.
|
||||
- Be granular. If you are investigating with multiple steps, report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input.
|
||||
- Use "state": "complete" only when finished with a task.
|
||||
- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing.
|
||||
- Include clear and actionable steps for the user.
|
||||
- Be less than 160 characters in length.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Amazon Q."
|
||||
default = "Please help me with my coding tasks. I'll provide specific instructions as needed."
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
# We need to use allowed tools to limit the context Amazon Q receives.
|
||||
# Amazon Q can't handle big contexts, and the `create_template_version` tool
|
||||
# has a description that's too long.
|
||||
mcp_json = <<EOT
|
||||
{
|
||||
"mcpServers": {
|
||||
"coder": {
|
||||
"command": "coder",
|
||||
"args": ["exp", "mcp", "server", "--allowed-tools", "coder_report_task"],
|
||||
"env": {
|
||||
"CODER_MCP_APP_STATUS_SLUG": "amazon-q"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOT
|
||||
encoded_mcp_json = base64encode(local.mcp_json)
|
||||
full_prompt = <<-EOT
|
||||
${var.system_prompt}
|
||||
|
||||
Your first task is:
|
||||
|
||||
${var.ai_prompt}
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_script" "amazon_q" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Amazon Q"
|
||||
icon = var.icon
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.install_amazon_q}" = "true" ]; then
|
||||
echo "Installing Amazon Q..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
Q_URL="https://desktop-release.q.us-east-1.amazonaws.com/${var.amazon_q_version}/q-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64"|"arm64")
|
||||
Q_URL="https://desktop-release.codewhisperer.us-east-1.amazonaws.com/${var.amazon_q_version}/q-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Amazon Q only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Amazon Q for $ARCH..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$Q_URL" -o "q.zip"
|
||||
unzip q.zip
|
||||
./q/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Amazon Q version: $(q --version)"
|
||||
fi
|
||||
|
||||
echo "Extracting auth tarball..."
|
||||
PREV_DIR="$PWD"
|
||||
echo "${var.experiment_auth_tarball}" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/amazon-q
|
||||
mkdir -p ~/.local/share/amazon-q
|
||||
cd ~/.local/share/amazon-q
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball"
|
||||
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Amazon Q to report tasks via Coder MCP..."
|
||||
mkdir -p ~/.aws/amazonq
|
||||
echo "${local.encoded_mcp_json}" | base64 -d > ~/.aws/amazonq/mcp.json
|
||||
echo "Created the ~/.aws/amazonq/mcp.json configuration file"
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background with tmux..."
|
||||
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
|
||||
|
||||
tmux send-keys -t amazon-q "${local.full_prompt}"
|
||||
sleep 5
|
||||
tmux send-keys -t amazon-q Enter
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Amazon Q in the background..."
|
||||
|
||||
if ! command_exists screen; then
|
||||
echo "Error: screen is not installed. Please install screen manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.amazon-q.log"
|
||||
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "multiuser on" >> "$HOME/.screenrc"
|
||||
fi
|
||||
|
||||
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS amazon-q bash -c '
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
|
||||
exec bash
|
||||
'
|
||||
# Extremely hacky way to send the prompt to the screen session
|
||||
# This will be fixed in the future, but `amazon-q` was not sending MCP
|
||||
# tasks when an initial prompt is provided.
|
||||
screen -S amazon-q -X stuff "${local.full_prompt}"
|
||||
sleep 5
|
||||
screen -S amazon-q -X stuff "^M"
|
||||
else
|
||||
if ! command_exists q; then
|
||||
echo "Error: Amazon Q is not installed. Please enable install_amazon_q or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "amazon_q" {
|
||||
slug = "amazon-q"
|
||||
display_name = "Amazon Q"
|
||||
agent_id = var.agent_id
|
||||
command = <<-EOT
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t amazon-q 2>/dev/null; then
|
||||
echo "Attaching to existing Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux attach-session -t amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q tmux session." | tee -a "$HOME/.amazon-q.log"
|
||||
tmux new-session -s amazon-q -c ${var.folder} "q chat --trust-all-tools | tee -a \"$HOME/.amazon-q.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "amazon-q"; then
|
||||
echo "Attaching to existing Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -xRR amazon-q
|
||||
else
|
||||
echo "Starting a new Amazon Q screen session." | tee -a "$HOME/.amazon-q.log"
|
||||
screen -S amazon-q bash -c 'q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
q chat --trust-all-tools
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
}
|
||||
@@ -17,7 +17,7 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
source = "registry.coder.com/coder/aws-region/coder"
|
||||
version = "1.0.12"
|
||||
default = "us-east-1"
|
||||
}
|
||||
@@ -38,7 +38,7 @@ Change the display name and icon for a region using the corresponding maps:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
source = "registry.coder.com/coder/aws-region/coder"
|
||||
version = "1.0.12"
|
||||
default = "ap-south-1"
|
||||
|
||||
@@ -65,7 +65,7 @@ Hide the Asia Pacific regions Seoul and Osaka:
|
||||
```tf
|
||||
module "aws-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/aws-region/coder"
|
||||
source = "registry.coder.com/coder/aws-region/coder"
|
||||
version = "1.0.12"
|
||||
exclude = ["ap-northeast-2", "ap-northeast-3"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele
|
||||
```tf
|
||||
module "azure_region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
source = "registry.coder.com/coder/azure-region/coder"
|
||||
version = "1.0.12"
|
||||
default = "eastus"
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Change the display name and icon for a region using the corresponding maps:
|
||||
```tf
|
||||
module "azure-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
source = "registry.coder.com/coder/azure-region/coder"
|
||||
version = "1.0.12"
|
||||
custom_names = {
|
||||
"australia" : "Go Australia!"
|
||||
@@ -59,7 +59,7 @@ Hide all regions in Australia except australiacentral:
|
||||
```tf
|
||||
module "azure-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/azure-region/coder"
|
||||
source = "registry.coder.com/coder/azure-region/coder"
|
||||
version = "1.0.12"
|
||||
exclude = [
|
||||
"australia",
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Claude Code in your workspace
|
||||
icon: ../../../../.icons/claude.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, claude-code]
|
||||
tags: [agent, claude-code, ai]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -13,8 +13,8 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -22,10 +22,10 @@ module "claude-code" {
|
||||
}
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and npm must be installed in your workspace to install Claude Code
|
||||
- `screen` must be installed in your workspace to run Claude Code in the background
|
||||
- Either `screen` or `tmux` must be installed in your workspace to run Claude Code in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
|
||||
@@ -43,7 +43,7 @@ The `codercom/oss-dogfood:latest` container image can be used for testing on con
|
||||
> Join our [Discord channel](https://discord.gg/coder) or
|
||||
> [contact us](https://coder.com/contact) to get help or share feedback.
|
||||
|
||||
Your workspace must have `screen` installed to use this.
|
||||
Your workspace must have either `screen` or `tmux` installed to use this.
|
||||
|
||||
```tf
|
||||
variable "anthropic_api_key" {
|
||||
@@ -54,7 +54,7 @@ variable "anthropic_api_key" {
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -71,7 +71,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
CODER_MCP_APP_STATUS_SLUG = "claude-code"
|
||||
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
|
||||
@@ -82,15 +82,15 @@ resource "coder_agent" "main" {
|
||||
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "0.2.57"
|
||||
|
||||
# Enable experimental features
|
||||
experiment_use_screen = true
|
||||
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
@@ -101,8 +101,8 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/modules/claude-code/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@@ -54,12 +54,35 @@ variable "experiment_use_screen" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_use_tmux" {
|
||||
type = bool
|
||||
description = "Whether to use tmux instead of screen for running Claude Code in the background."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Claude Code."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
resource "coder_script" "claude_code" {
|
||||
agent_id = var.agent_id
|
||||
@@ -74,6 +97,14 @@ resource "coder_script" "claude_code" {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Claude Code if enabled
|
||||
if [ "${var.install_claude_code}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
@@ -84,11 +115,52 @@ resource "coder_script" "claude_code" {
|
||||
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
coder exp mcp configure claude-code ${var.folder}
|
||||
fi
|
||||
|
||||
# Handle terminal multiplexer selection (tmux or screen)
|
||||
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
|
||||
echo "Please set only one of them to true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
echo "Running Claude Code in the background with tmux..."
|
||||
|
||||
# Check if tmux is installed
|
||||
if ! command_exists tmux; then
|
||||
echo "Error: tmux is not installed. Please install tmux manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
|
||||
|
||||
# Send the prompt to the tmux session if needed
|
||||
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
|
||||
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
sleep 5
|
||||
tmux send-keys -t claude-code Enter
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Claude Code in the background..."
|
||||
@@ -149,20 +221,27 @@ resource "coder_app" "claude_code" {
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t claude-code 2>/dev/null; then
|
||||
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux attach-session -t claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "claude-code"; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log"
|
||||
echo "Attaching to existing Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -xRR claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
claude
|
||||
fi
|
||||
EOT
|
||||
|
||||
@@ -4,7 +4,7 @@ description: VS Code in the browser
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, ide, web]
|
||||
tags: [ide, web, code-server]
|
||||
---
|
||||
|
||||
# code-server
|
||||
@@ -14,8 +14,8 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,8 +29,8 @@ module "code-server" {
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_version = "4.8.3"
|
||||
}
|
||||
@@ -43,8 +43,8 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
@@ -61,8 +61,8 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -78,8 +78,8 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
@@ -94,8 +94,8 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
use_cached = true
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
@@ -107,8 +107,8 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
```tf
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,12 @@ variable "settings" {
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "machine-settings" {
|
||||
type = any
|
||||
description = "A map of template level machine settings to apply to code-server. This will be overwritten at each container start."
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in code-server."
|
||||
@@ -122,6 +128,20 @@ variable "subdomain" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "open_in" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`.
|
||||
`"tab"` opens in a new tab in the same browser window.
|
||||
`"slim-window"` opens a new browser window without navigation controls.
|
||||
EOT
|
||||
default = "slim-window"
|
||||
validation {
|
||||
condition = contains(["tab", "slim-window"], var.open_in)
|
||||
error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'."
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_script" "code-server" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "code-server"
|
||||
@@ -135,6 +155,7 @@ resource "coder_script" "code-server" {
|
||||
INSTALL_PREFIX : var.install_prefix,
|
||||
// This is necessary otherwise the quotes are stripped!
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
MACHINE_SETTINGS : replace(jsonencode(var.machine-settings), "\"", "\\\""),
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
USE_CACHED_EXTENSIONS : var.use_cached_extensions,
|
||||
@@ -166,6 +187,7 @@ resource "coder_app" "code-server" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
open_in = var.open_in
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.port}/healthz"
|
||||
|
||||
@@ -23,7 +23,20 @@ function run_code_server() {
|
||||
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
|
||||
if command -v jq &> /dev/null; then
|
||||
echo "${SETTINGS}" | jq '.' > ~/.local/share/code-server/User/settings.json
|
||||
else
|
||||
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply/overwrite template based settings
|
||||
echo "⚙️ Creating machine settings file..."
|
||||
mkdir -p ~/.local/share/code-server/Machine
|
||||
if command -v jq &> /dev/null; then
|
||||
echo "${MACHINE_SETTINGS}" | jq '.' > ~/.local/share/code-server/Machine/settings.json
|
||||
else
|
||||
echo "${MACHINE_SETTINGS}" > ~/.local/share/code-server/Machine/settings.json
|
||||
fi
|
||||
|
||||
# Check if code-server is already installed for offline
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically logs the user into Coder when creating their workspace.
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch Cursor IDE
|
||||
icon: ../../../../.icons/cursor.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, cursor, helper]
|
||||
tags: [ide, cursor, ai]
|
||||
---
|
||||
|
||||
# Cursor IDE
|
||||
@@ -16,8 +16,8 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/cursor/coder"
|
||||
version = "1.0.19"
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,8 +29,8 @@ module "cursor" {
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/cursor/coder"
|
||||
version = "1.0.19"
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,18 @@ variable "order" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "cursor"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Cursor Desktop"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
@@ -39,8 +51,8 @@ resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/cursor.svg"
|
||||
slug = "cursor"
|
||||
display_name = "Cursor Desktop"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
url = join("", [
|
||||
"cursor://coder.coder-remote/open",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: devcontainers-cli
|
||||
description: devcontainers-cli module provides an easy way to install @devcontainers/cli into a workspace
|
||||
icon: ../../../../.icons/devcontainers.svg
|
||||
verified: true
|
||||
maintainer_github: coder
|
||||
tags: [devcontainers]
|
||||
---
|
||||
|
||||
# devcontainers-cli
|
||||
|
||||
The devcontainers-cli module provides an easy way to install [`@devcontainers/cli`](https://github.com/devcontainers/cli) into a workspace. It can be used within any workspace as it runs only if
|
||||
@devcontainers/cli is not installed yet.
|
||||
`npm` is required and should be pre-installed in order for the module to work.
|
||||
|
||||
```tf
|
||||
module "devcontainers-cli" {
|
||||
source = "registry.coder.com/coder/devcontainers-cli/coder"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
execContainer,
|
||||
executeScriptInContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
type TerraformState,
|
||||
} from "~test";
|
||||
|
||||
const executeScriptInContainerWithPackageManager = async (
|
||||
state: TerraformState,
|
||||
image: string,
|
||||
packageManager: string,
|
||||
shell = "sh",
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}> => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
// Install the specified package manager
|
||||
if (packageManager === "npm") {
|
||||
await execContainer(id, [shell, "-c", "apk add nodejs npm"]);
|
||||
} else if (packageManager === "pnpm") {
|
||||
await execContainer(id, [
|
||||
shell,
|
||||
"-c",
|
||||
`wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.shrc" SHELL="$(which sh)" sh -`,
|
||||
]);
|
||||
} else if (packageManager === "yarn") {
|
||||
await execContainer(id, [
|
||||
shell,
|
||||
"-c",
|
||||
"apk add nodejs npm && npm install -g yarn",
|
||||
]);
|
||||
}
|
||||
|
||||
const pathResp = await execContainer(id, [shell, "-c", "echo $PATH"]);
|
||||
const path = pathResp.stdout.trim();
|
||||
|
||||
console.log(path);
|
||||
|
||||
const resp = await execContainer(
|
||||
id,
|
||||
[shell, "-c", instance.script],
|
||||
[
|
||||
"--env",
|
||||
"CODER_SCRIPT_BIN_DIR=/tmp/coder-script-data/bin",
|
||||
"--env",
|
||||
`PATH=${path}:/tmp/coder-script-data/bin`,
|
||||
],
|
||||
);
|
||||
const stdout = resp.stdout.trim().split("\n");
|
||||
const stderr = resp.stderr.trim().split("\n");
|
||||
return {
|
||||
exitCode: resp.exitCode,
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
};
|
||||
|
||||
describe("devcontainers-cli", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
it("misses all package managers", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
const output = await executeScriptInContainer(state, "docker:dind");
|
||||
expect(output.exitCode).toBe(1);
|
||||
expect(output.stderr).toEqual([
|
||||
"ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first.",
|
||||
]);
|
||||
}, 15000);
|
||||
|
||||
it("installs devcontainers-cli with npm", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"docker:dind",
|
||||
"npm",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"Installing @devcontainers/cli using npm...",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
it("installs devcontainers-cli with yarn", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"docker:dind",
|
||||
"yarn",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"Installing @devcontainers/cli using yarn...",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /tmp/coder-script-data/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
|
||||
it("displays warning if docker is not installed", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
});
|
||||
|
||||
const output = await executeScriptInContainerWithPackageManager(
|
||||
state,
|
||||
"alpine",
|
||||
"npm",
|
||||
);
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
expect(output.stdout[0]).toEqual(
|
||||
"WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available.",
|
||||
);
|
||||
expect(output.stdout[output.stdout.length - 1]).toEqual(
|
||||
"🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!",
|
||||
);
|
||||
}, 15000);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
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."
|
||||
}
|
||||
|
||||
resource "coder_script" "devcontainers-cli" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "devcontainers-cli"
|
||||
icon = "/icon/devcontainers.svg"
|
||||
script = templatefile("${path.module}/run.sh", {})
|
||||
run_on_start = true
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# If @devcontainers/cli is already installed, we can skip
|
||||
if command -v devcontainer >/dev/null 2>&1; then
|
||||
echo "🥳 @devcontainers/cli is already installed into $(which devcontainer)!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if docker is installed
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available."
|
||||
fi
|
||||
|
||||
# Determine the package manager to use: npm, pnpm, or yarn
|
||||
if command -v yarn >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="yarn"
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="npm"
|
||||
elif command -v pnpm >/dev/null 2>&1; then
|
||||
PACKAGE_MANAGER="pnpm"
|
||||
else
|
||||
echo "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install() {
|
||||
echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..."
|
||||
if [ "$PACKAGE_MANAGER" = "npm" ]; then
|
||||
npm install -g @devcontainers/cli
|
||||
elif [ "$PACKAGE_MANAGER" = "pnpm" ]; then
|
||||
# Check if PNPM_HOME is set, if not, set it to the script's bin directory
|
||||
# pnpm needs this to be set to install binaries
|
||||
# coder agent ensures this part is part of the PATH
|
||||
# so that the devcontainer command is available
|
||||
if [ -z "$PNPM_HOME" ]; then
|
||||
PNPM_HOME="$CODER_SCRIPT_BIN_DIR"
|
||||
export M_HOME
|
||||
fi
|
||||
pnpm add -g @devcontainers/cli
|
||||
elif [ "$PACKAGE_MANAGER" = "yarn" ]; then
|
||||
yarn global add @devcontainers/cli --prefix "$(dirname "$CODER_SCRIPT_BIN_DIR")"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! install; then
|
||||
echo "Failed to install @devcontainers/cli" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v devcontainer >/dev/null 2>&1; then
|
||||
echo "Installation completed but 'devcontainer' command not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🥳 @devcontainers/cli has been installed into $(which devcontainer)!"
|
||||
exit 0
|
||||
@@ -4,7 +4,7 @@ description: Allow developers to optionally bring their own dotfiles repository
|
||||
icon: ../../../../.icons/dotfiles.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, dotfiles]
|
||||
---
|
||||
|
||||
# Dotfiles
|
||||
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -31,7 +31,7 @@ module "dotfiles" {
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "dotfiles" {
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
@@ -54,14 +54,14 @@ module "dotfiles" {
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "dotfiles-root" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
user = "root"
|
||||
@@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_
|
||||
```tf
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.0.29"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A file browser for your workspace
|
||||
icon: ../../../../.icons/filebrowser.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, filebrowser]
|
||||
tags: [filebrowser, web]
|
||||
---
|
||||
|
||||
# File Browser
|
||||
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
||||
```tf
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/filebrowser/coder"
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -29,7 +29,7 @@ module "filebrowser" {
|
||||
```tf
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/filebrowser/coder"
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
@@ -41,7 +41,7 @@ module "filebrowser" {
|
||||
```tf
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/filebrowser/coder"
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
@@ -53,7 +53,7 @@ module "filebrowser" {
|
||||
```tf
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/filebrowser/coder"
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "main"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
executeScriptInContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
type scriptOutput,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
function testBaseLine(output: scriptOutput) {
|
||||
expect(output.exitCode).toBe(0);
|
||||
|
||||
const expectedLines = [
|
||||
"\u001b[[0;1mInstalling filebrowser ",
|
||||
"🥳 Installation complete! ",
|
||||
"👷 Starting filebrowser in background... ",
|
||||
"📂 Serving /root at http://localhost:13339 ",
|
||||
"📝 Logs at /tmp/filebrowser.log",
|
||||
];
|
||||
|
||||
// we could use expect(output.stdout).toEqual(expect.arrayContaining(expectedLines)), but when it errors, it doesn't say which line is wrong
|
||||
for (const line of expectedLines) {
|
||||
expect(output.stdout).toContain(line);
|
||||
}
|
||||
}
|
||||
|
||||
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/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
|
||||
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 await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
|
||||
it("runs with folder var", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/home/coder/project",
|
||||
});
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs with subdomain=false", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "main",
|
||||
subdomain: false,
|
||||
});
|
||||
|
||||
const output = await await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/curl",
|
||||
"sh",
|
||||
"apk add bash",
|
||||
);
|
||||
|
||||
testBaseLine(output);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[[0;1m'
|
||||
|
||||
printf "$${BOLD}Installing filebrowser \n\n"
|
||||
|
||||
# Check if filebrowser is installed
|
||||
if ! command -v filebrowser &> /dev/null; then
|
||||
if ! command -v filebrowser &>/dev/null; then
|
||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||
fi
|
||||
|
||||
@@ -32,6 +34,6 @@ printf "👷 Starting filebrowser in background... \n\n"
|
||||
|
||||
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
|
||||
|
||||
filebrowser >> ${LOG_PATH} 2>&1 &
|
||||
filebrowser >>${LOG_PATH} 2>&1 &
|
||||
|
||||
printf "📝 Logs at ${LOG_PATH} \n\n"
|
||||
|
||||
@@ -16,7 +16,7 @@ We can use the simplest format here, only adding a default selection as the `atl
|
||||
```tf
|
||||
module "fly-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/fly-region/coder"
|
||||
source = "registry.coder.com/coder/fly-region/coder"
|
||||
version = "1.0.2"
|
||||
default = "atl"
|
||||
}
|
||||
@@ -33,7 +33,7 @@ The regions argument can be used to display only the desired regions in the Code
|
||||
```tf
|
||||
module "fly-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/fly-region/coder"
|
||||
source = "registry.coder.com/coder/fly-region/coder"
|
||||
version = "1.0.2"
|
||||
default = "ams"
|
||||
regions = ["ams", "arn", "atl"]
|
||||
@@ -49,7 +49,7 @@ Set custom icons and names with their respective maps.
|
||||
```tf
|
||||
module "fly-region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/fly-region/coder"
|
||||
source = "registry.coder.com/coder/fly-region/coder"
|
||||
version = "1.0.2"
|
||||
default = "ams"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This module adds Google Cloud Platform regions to your Coder template.
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
source = "registry.coder.com/coder/gcp-region/coder"
|
||||
version = "1.0.12"
|
||||
regions = ["us", "europe"]
|
||||
}
|
||||
@@ -35,7 +35,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support,
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
source = "registry.coder.com/coder/gcp-region/coder"
|
||||
version = "1.0.12"
|
||||
default = ["us-west1-a"]
|
||||
regions = ["us-west1"]
|
||||
@@ -52,7 +52,7 @@ resource "google_compute_instance" "example" {
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
source = "registry.coder.com/coder/gcp-region/coder"
|
||||
version = "1.0.12"
|
||||
regions = ["europe-west"]
|
||||
single_zone_per_region = false
|
||||
@@ -68,7 +68,7 @@ resource "google_compute_instance" "example" {
|
||||
```tf
|
||||
module "gcp_region" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/gcp-region/coder"
|
||||
source = "registry.coder.com/coder/gcp-region/coder"
|
||||
version = "1.0.12"
|
||||
regions = ["us", "europe"]
|
||||
gpu_only = true
|
||||
|
||||
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
@@ -28,7 +28,7 @@ module "git-clone" {
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
@@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
@@ -69,7 +69,7 @@ data "coder_parameter" "git_repo" {
|
||||
# Clone the repository for branch `feat/example`
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
@@ -78,7 +78,7 @@ module "git_clone" {
|
||||
# Create a code-server instance for the cloned repository
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
order = 1
|
||||
@@ -103,7 +103,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
@@ -122,7 +122,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
@@ -134,7 +134,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
@@ -155,7 +155,7 @@ For example, to clone the `feat/example` branch:
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
@@ -172,7 +172,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
|
||||
@@ -9,7 +9,7 @@ tags: [helper, git]
|
||||
|
||||
# git-commit-signing
|
||||
|
||||
> [!IMPORTANT]
|
||||
> [!IMPORTANT]
|
||||
> This module will only work with Git versions >=2.34, prior versions [do not support signing commits via SSH keys](https://lore.kernel.org/git/xmqq8rxpgwki.fsf@gitster.g/).
|
||||
|
||||
This module downloads your SSH key from Coder and uses it to sign commits with Git.
|
||||
@@ -22,7 +22,7 @@ This module has a chance of conflicting with the user's dotfiles / the personali
|
||||
```tf
|
||||
module "git-commit-signing" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-commit-signing/coder"
|
||||
source = "registry.coder.com/coder/git-commit-signing/coder"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Runs a script that updates git credentials in the workspace to match the user's
|
||||
```tf
|
||||
module "git-config" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
source = "registry.coder.com/coder/git-config/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -29,7 +29,7 @@ TODO: Add screenshot
|
||||
```tf
|
||||
module "git-config" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
source = "registry.coder.com/coder/git-config/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
allow_email_change = true
|
||||
@@ -43,7 +43,7 @@ TODO: Add screenshot
|
||||
```tf
|
||||
module "git-config" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
source = "registry.coder.com/coder/git-config/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
allow_username_change = false
|
||||
|
||||
@@ -14,19 +14,19 @@ Templates that utilize Github External Auth can automatically ensure that the Co
|
||||
```tf
|
||||
module "github-upload-public-key" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
source = "registry.coder.com/coder/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
# Requirements
|
||||
## Requirements
|
||||
|
||||
This module requires `curl` and `jq` to be installed inside your workspace.
|
||||
|
||||
Github External Auth must be enabled in the workspace for this module to work. The Github app that is configured for external auth must have both read and write permissions to "Git SSH keys" in order to upload the public key. Additionally, a Coder admin must also have the `admin:public_key` scope added to the external auth configuration of the Coder deployment. For example:
|
||||
|
||||
```
|
||||
```txt
|
||||
CODER_EXTERNAL_AUTH_0_ID="USER_DEFINED_ID"
|
||||
CODER_EXTERNAL_AUTH_0_TYPE=github
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx
|
||||
@@ -36,7 +36,7 @@ CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key"
|
||||
|
||||
Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes.
|
||||
|
||||
# Example
|
||||
## Example
|
||||
|
||||
Using a coder github external auth with a non-default id: (default is `github`)
|
||||
|
||||
@@ -47,7 +47,7 @@ data "coder_external_auth" "github" {
|
||||
|
||||
module "github-upload-public-key" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
source = "registry.coder.com/coder/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
external_auth_id = data.coder_external_auth.github.id
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Goose in your workspace
|
||||
icon: ../../../../.icons/goose.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, goose]
|
||||
tags: [agent, goose, ai]
|
||||
---
|
||||
|
||||
# Goose
|
||||
@@ -13,8 +13,8 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -22,7 +22,7 @@ module "goose" {
|
||||
}
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
- `screen` must be installed in your workspace to run Goose in the background
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
@@ -47,7 +47,7 @@ Your workspace must have `screen` installed to use this.
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -72,11 +72,11 @@ resource "coder_agent" "main" {
|
||||
env = {
|
||||
GOOSE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help write code.
|
||||
|
||||
|
||||
Run all long running tasks (e.g. npm run dev) in the background and not in the foreground.
|
||||
|
||||
|
||||
Periodically check in on background tasks.
|
||||
|
||||
|
||||
Notify Coder of the status of the task before and after your steps.
|
||||
EOT
|
||||
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
@@ -89,8 +89,8 @@ resource "coder_agent" "main" {
|
||||
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -111,14 +111,44 @@ module "goose" {
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Custom Extensions (MCP)
|
||||
|
||||
You can extend Goose's capabilities by adding custom extensions. For example, to add the desktop-commander extension:
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
# ... other configuration ...
|
||||
|
||||
experiment_pre_install_script = <<-EOT
|
||||
npm i -g @wonderwhy-er/desktop-commander@latest
|
||||
EOT
|
||||
|
||||
experiment_additional_extensions = <<-EOT
|
||||
desktop-commander:
|
||||
args: []
|
||||
cmd: desktop-commander
|
||||
description: Ideal for background tasks
|
||||
enabled: true
|
||||
envs: {}
|
||||
name: desktop-commander
|
||||
timeout: 300
|
||||
type: stdio
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
This will add the desktop-commander extension to Goose, allowing it to run commands in the background. The extension will be available in the Goose interface and can be used to run long-running processes like development servers.
|
||||
|
||||
Note: The indentation in the heredoc is preserved, so you can write the YAML naturally.
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/modules/goose/coder"
|
||||
version = "1.0.31"
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@@ -69,15 +69,69 @@ variable "experiment_auto_configure" {
|
||||
variable "experiment_goose_provider" {
|
||||
type = string
|
||||
description = "The provider to use for Goose (e.g., anthropic)."
|
||||
default = null
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_goose_model" {
|
||||
type = string
|
||||
description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Goose."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_additional_extensions" {
|
||||
type = string
|
||||
description = "Additional extensions configuration in YAML format to append to the config."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) you are working on.
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
EOT
|
||||
|
||||
# Add two spaces to each line of extensions to match YAML structure
|
||||
formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}"
|
||||
additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : ""
|
||||
|
||||
combined_extensions = <<-EOT
|
||||
extensions:
|
||||
${local.formatted_base}${local.additional_extensions}
|
||||
EOT
|
||||
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
@@ -92,6 +146,14 @@ resource "coder_script" "goose" {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
chmod +x /tmp/pre_install.sh
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Goose if enabled
|
||||
if [ "${var.install_goose}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
@@ -102,6 +164,14 @@ resource "coder_script" "goose" {
|
||||
RELEASE_TAG=v${var.goose_version} curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
if [ -n "${local.encoded_post_install_script}" ]; then
|
||||
echo "Running post-install script..."
|
||||
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
|
||||
chmod +x /tmp/post_install.sh
|
||||
/tmp/post_install.sh
|
||||
fi
|
||||
|
||||
# Configure Goose if auto-configure is enabled
|
||||
if [ "${var.experiment_auto_configure}" = "true" ]; then
|
||||
echo "Configuring Goose..."
|
||||
@@ -109,29 +179,14 @@ resource "coder_script" "goose" {
|
||||
cat > "$HOME/.config/goose/config.yaml" << EOL
|
||||
GOOSE_PROVIDER: ${var.experiment_goose_provider}
|
||||
GOOSE_MODEL: ${var.experiment_goose_model}
|
||||
extensions:
|
||||
coder:
|
||||
args:
|
||||
- exp
|
||||
- mcp
|
||||
- server
|
||||
cmd: coder
|
||||
description: Report ALL tasks and statuses (in progress, done, failed) before and after starting
|
||||
enabled: true
|
||||
envs:
|
||||
CODER_MCP_APP_STATUS_SLUG: goose
|
||||
name: Coder
|
||||
timeout: 3000
|
||||
type: stdio
|
||||
developer:
|
||||
display_name: Developer
|
||||
enabled: true
|
||||
name: developer
|
||||
timeout: 300
|
||||
type: builtin
|
||||
${trimspace(local.combined_extensions)}
|
||||
EOL
|
||||
fi
|
||||
|
||||
# Write system prompt to config
|
||||
mkdir -p "$HOME/.config/goose"
|
||||
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
echo "Running Goose in the background..."
|
||||
@@ -162,14 +217,28 @@ EOL
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
screen -U -dmS goose bash -c '
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
screen -U -dmS goose bash -c "
|
||||
cd ${var.folder}
|
||||
$HOME/.local/bin/goose run --text "$GOOSE_SYSTEM_PROMPT. Your task: $GOOSE_TASK_PROMPT" --interactive | tee -a "$HOME/.goose.log"
|
||||
exec bash
|
||||
'
|
||||
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
|
||||
/bin/bash
|
||||
"
|
||||
else
|
||||
# Check if goose is installed before running
|
||||
if ! command_exists $HOME/.local/bin/goose; then
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
@@ -186,21 +255,34 @@ resource "coder_app" "goose" {
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Determine goose command
|
||||
if command_exists goose; then
|
||||
GOOSE_CMD=goose
|
||||
elif [ -f "$HOME/.local/bin/goose" ]; then
|
||||
GOOSE_CMD="$HOME/.local/bin/goose"
|
||||
else
|
||||
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "goose"; then
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
echo "Attaching to existing Goose session." | tee -a "$HOME/.goose.log"
|
||||
screen -xRR goose
|
||||
else
|
||||
echo "Starting a new Goose session." | tee -a "$HOME/.goose.log"
|
||||
screen -S goose bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; $HOME/.local/bin/goose run --text "Always report status and instructions to Coder, before and after your steps" --interactive | tee -a "$HOME/.goose.log"; exec bash'
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "goose"; then
|
||||
echo "Error: No existing Goose session found. Please wait for the script to start it."
|
||||
exit 1
|
||||
fi
|
||||
# Only attach to existing session
|
||||
screen -xRR goose
|
||||
else
|
||||
cd ${var.folder}
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
$HOME/.local/bin/goose
|
||||
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, hashicorp, hvs]
|
||||
tags: [integration, vault, hashicorp, hvs]
|
||||
---
|
||||
|
||||
# HCP Vault Secrets
|
||||
@@ -14,7 +14,7 @@ This module lets you fetch all or selective secrets from a [HCP Vault Secrets](h
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
@@ -40,7 +40,7 @@ To fetch all secrets from the HCP Vault Secrets app, skip the `secrets` input.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
@@ -54,7 +54,7 @@ To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` inp
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
@@ -69,7 +69,7 @@ Set `client_id` and `client_secret` as module inputs.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch JetBrains Gateway IDEs in the dash
|
||||
icon: ../../../../.icons/gateway.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, jetbrains, helper, parameter]
|
||||
tags: [ide, jetbrains, parameter, gateway]
|
||||
---
|
||||
|
||||
# JetBrains Gateway
|
||||
@@ -17,8 +17,8 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||
@@ -26,7 +26,7 @@ module "jetbrains_gateway" {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
@@ -35,8 +35,8 @@ module "jetbrains_gateway" {
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -49,8 +49,8 @@ module "jetbrains_gateway" {
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -64,8 +64,8 @@ module "jetbrains_gateway" {
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["IU", "PY"]
|
||||
@@ -89,8 +89,8 @@ module "jetbrains_gateway" {
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
@@ -107,8 +107,8 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
|
||||
```tf
|
||||
module "jetbrains_gateway" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||
version = "1.0.28"
|
||||
source = "registry.coder.com/coder/jetbrains-gateway/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/example"
|
||||
jetbrains_ides = ["GO", "WS"]
|
||||
|
||||
@@ -13,6 +13,16 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
variable "arch" {
|
||||
type = string
|
||||
description = "The target architecture of the workspace"
|
||||
default = "amd64"
|
||||
validation {
|
||||
condition = contains(["amd64", "arm64"], var.arch)
|
||||
error_message = "Architecture must be either 'amd64' or 'arm64'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@@ -178,78 +188,100 @@ data "http" "jetbrains_ide_versions" {
|
||||
}
|
||||
|
||||
locals {
|
||||
# AMD64 versions of the images just use the version string, while ARM64
|
||||
# versions append "-aarch64". Eg:
|
||||
#
|
||||
# https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz
|
||||
# https://download.jetbrains.com/idea/ideaIU-2025.1.tar.gz
|
||||
#
|
||||
# We rewrite the data map above dynamically based on the user's architecture parameter.
|
||||
#
|
||||
effective_jetbrains_ide_versions = {
|
||||
for k, v in var.jetbrains_ide_versions : k => {
|
||||
build_number = v.build_number
|
||||
version = var.arch == "arm64" ? "${v.version}-aarch64" : v.version
|
||||
}
|
||||
}
|
||||
|
||||
# When downloading the latest IDE, the download link in the JSON is either:
|
||||
#
|
||||
# linux.download_link
|
||||
# linuxARM64.download_link
|
||||
#
|
||||
download_key = var.arch == "arm64" ? "linuxARM64" : "linux"
|
||||
|
||||
jetbrains_ides = {
|
||||
"GO" = {
|
||||
icon = "/icon/goland.svg",
|
||||
name = "GoLand",
|
||||
identifier = "GO",
|
||||
build_number = var.jetbrains_ide_versions["GO"].build_number,
|
||||
download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["GO"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["GO"].build_number,
|
||||
download_link = "${var.download_base_link}/go/goland-${local.effective_jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["GO"].version
|
||||
},
|
||||
"WS" = {
|
||||
icon = "/icon/webstorm.svg",
|
||||
name = "WebStorm",
|
||||
identifier = "WS",
|
||||
build_number = var.jetbrains_ide_versions["WS"].build_number,
|
||||
download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["WS"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["WS"].build_number,
|
||||
download_link = "${var.download_base_link}/webstorm/WebStorm-${local.effective_jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["WS"].version
|
||||
},
|
||||
"IU" = {
|
||||
icon = "/icon/intellij.svg",
|
||||
name = "IntelliJ IDEA Ultimate",
|
||||
identifier = "IU",
|
||||
build_number = var.jetbrains_ide_versions["IU"].build_number,
|
||||
download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["IU"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["IU"].build_number,
|
||||
download_link = "${var.download_base_link}/idea/ideaIU-${local.effective_jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["IU"].version
|
||||
},
|
||||
"PY" = {
|
||||
icon = "/icon/pycharm.svg",
|
||||
name = "PyCharm Professional",
|
||||
identifier = "PY",
|
||||
build_number = var.jetbrains_ide_versions["PY"].build_number,
|
||||
download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PY"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["PY"].build_number,
|
||||
download_link = "${var.download_base_link}/python/pycharm-professional-${local.effective_jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["PY"].version
|
||||
},
|
||||
"CL" = {
|
||||
icon = "/icon/clion.svg",
|
||||
name = "CLion",
|
||||
identifier = "CL",
|
||||
build_number = var.jetbrains_ide_versions["CL"].build_number,
|
||||
download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["CL"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["CL"].build_number,
|
||||
download_link = "${var.download_base_link}/cpp/CLion-${local.effective_jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["CL"].version
|
||||
},
|
||||
"PS" = {
|
||||
icon = "/icon/phpstorm.svg",
|
||||
name = "PhpStorm",
|
||||
identifier = "PS",
|
||||
build_number = var.jetbrains_ide_versions["PS"].build_number,
|
||||
download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["PS"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["PS"].build_number,
|
||||
download_link = "${var.download_base_link}/webide/PhpStorm-${local.effective_jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["PS"].version
|
||||
},
|
||||
"RM" = {
|
||||
icon = "/icon/rubymine.svg",
|
||||
name = "RubyMine",
|
||||
identifier = "RM",
|
||||
build_number = var.jetbrains_ide_versions["RM"].build_number,
|
||||
download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RM"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RM"].build_number,
|
||||
download_link = "${var.download_base_link}/ruby/RubyMine-${local.effective_jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RM"].version
|
||||
},
|
||||
"RD" = {
|
||||
icon = "/icon/rider.svg",
|
||||
name = "Rider",
|
||||
identifier = "RD",
|
||||
build_number = var.jetbrains_ide_versions["RD"].build_number,
|
||||
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RD"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RD"].build_number,
|
||||
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${local.effective_jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RD"].version
|
||||
},
|
||||
"RR" = {
|
||||
icon = "/icon/rustrover.svg",
|
||||
name = "RustRover",
|
||||
identifier = "RR",
|
||||
build_number = var.jetbrains_ide_versions["RR"].build_number,
|
||||
download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz"
|
||||
version = var.jetbrains_ide_versions["RR"].version
|
||||
build_number = local.effective_jetbrains_ide_versions["RR"].build_number,
|
||||
download_link = "${var.download_base_link}/rustrover/RustRover-${local.effective_jetbrains_ide_versions["RR"].version}.tar.gz"
|
||||
version = local.effective_jetbrains_ide_versions["RR"].version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +290,7 @@ locals {
|
||||
key = var.latest ? keys(local.json_data)[0] : ""
|
||||
display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name
|
||||
identifier = data.coder_parameter.jetbrains_ide.value
|
||||
download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
download_link = var.latest ? local.json_data[local.key][0].downloads[local.download_key].link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link
|
||||
build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number
|
||||
version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/jfrog.svg
|
||||
maintainer_github: coder
|
||||
partner_github: jfrog
|
||||
verified: true
|
||||
tags: [integration, jfrog]
|
||||
tags: [integration, jfrog, helper]
|
||||
---
|
||||
|
||||
# JFrog
|
||||
@@ -17,7 +17,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
@@ -46,7 +46,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
@@ -75,7 +75,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
|
||||
@@ -14,7 +14,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.0.30"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
@@ -41,7 +41,7 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.0.30"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://YYYY.jfrog.io"
|
||||
@@ -74,7 +74,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.0.30"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
@@ -94,7 +94,7 @@ module "jfrog" {
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
module "jfrog" {
|
||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||
source = "registry.coder.com/coder/jfrog-token/coder"
|
||||
version = "1.0.30"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://XXXX.jfrog.io"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A module that adds Jupyter Notebook in your Coder template.
|
||||
icon: ../../../../.icons/jupyter.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [jupyter, helper, ide, web]
|
||||
tags: [jupyter, ide, web]
|
||||
---
|
||||
|
||||
# Jupyter Notebook
|
||||
@@ -16,7 +16,7 @@ A module that adds Jupyter Notebook in your Coder template.
|
||||
```tf
|
||||
module "jupyter-notebook" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jupyter-notebook/coder"
|
||||
source = "registry.coder.com/coder/jupyter-notebook/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A module that adds JupyterLab in your Coder template.
|
||||
icon: ../../../../.icons/jupyter.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [jupyter, helper, ide, web]
|
||||
tags: [jupyter, ide, web]
|
||||
---
|
||||
|
||||
# JupyterLab
|
||||
@@ -16,8 +16,8 @@ A module that adds JupyterLab in your Coder template.
|
||||
```tf
|
||||
module "jupyterlab" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jupyterlab/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/jupyterlab/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,13 +3,13 @@ INSTALLER=""
|
||||
check_available_installer() {
|
||||
# check if pipx is installed
|
||||
echo "Checking for a supported installer"
|
||||
if command -v pipx > /dev/null 2>&1; then
|
||||
if command -v pipx >/dev/null 2>&1; then
|
||||
echo "pipx is installed"
|
||||
INSTALLER="pipx"
|
||||
return
|
||||
fi
|
||||
# check if uv is installed
|
||||
if command -v uv > /dev/null 2>&1; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
echo "uv is installed"
|
||||
INSTALLER="uv"
|
||||
return
|
||||
@@ -26,32 +26,33 @@ fi
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
# check if jupyterlab is installed
|
||||
if ! command -v jupyter-lab > /dev/null 2>&1; then
|
||||
if ! command -v jupyter-lab >/dev/null 2>&1; then
|
||||
# install jupyterlab
|
||||
check_available_installer
|
||||
printf "$${BOLD}Installing jupyterlab!\n"
|
||||
case $INSTALLER in
|
||||
uv)
|
||||
uv pip install -q jupyterlab \
|
||||
&& printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTERPATH="$HOME/.venv/bin/"
|
||||
;;
|
||||
pipx)
|
||||
pipx install jupyterlab \
|
||||
&& printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTERPATH="$HOME/.local/bin"
|
||||
;;
|
||||
uv)
|
||||
uv pip install -q jupyterlab &&
|
||||
printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTER="$HOME/.venv/bin/jupyter-lab"
|
||||
;;
|
||||
pipx)
|
||||
pipx install jupyterlab &&
|
||||
printf "%s\n" "🥳 jupyterlab has been installed"
|
||||
JUPYTER="$HOME/.local/bin/jupyter-lab"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf "%s\n\n" "🥳 jupyterlab is already installed"
|
||||
JUPYTER=$(command -v jupyter-lab)
|
||||
fi
|
||||
|
||||
printf "👷 Starting jupyterlab in background..."
|
||||
printf "check logs at ${LOG_PATH}"
|
||||
$JUPYTERPATH/jupyter-lab --no-browser \
|
||||
$JUPYTER --no-browser \
|
||||
"$BASE_URL_FLAG" \
|
||||
--ServerApp.ip='*' \
|
||||
--ServerApp.port="${PORT}" \
|
||||
--ServerApp.token='' \
|
||||
--ServerApp.password='' \
|
||||
> "${LOG_PATH}" 2>&1 &
|
||||
>"${LOG_PATH}" 2>&1 &
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A modern open source VNC server
|
||||
icon: ../../../../.icons/kasmvnc.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, vnc, desktop]
|
||||
tags: [vnc, desktop, kasmvnc]
|
||||
---
|
||||
|
||||
# KasmVNC
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
```tf
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/kasmvnc/coder"
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.0.23"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Allow developers to customize their workspace on start
|
||||
icon: ../../../../.icons/personalize.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, personalize]
|
||||
---
|
||||
|
||||
# Personalize
|
||||
@@ -14,7 +14,7 @@ Run a script on workspace start that allows developers to run custom commands to
|
||||
```tf
|
||||
module "personalize" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/personalize/coder"
|
||||
source = "registry.coder.com/coder/personalize/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
@@ -4,13 +4,23 @@ description: Send a Slack message when a command finishes inside a workspace!
|
||||
icon: ../../../../.icons/slack.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, slack]
|
||||
---
|
||||
|
||||
# Slack Me
|
||||
|
||||
Add the `slackme` command to your workspace that DMs you on Slack when your command finishes running.
|
||||
|
||||
```tf
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
auth_provider_id = "slack"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
slackme npm run long-build
|
||||
```
|
||||
@@ -54,16 +64,6 @@ slackme npm run long-build
|
||||
|
||||
3. Restart your Coder deployment. Any Template can now import the Slack Me module, and `slackme` will be available on the `$PATH`:
|
||||
|
||||
```tf
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/slackme/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
auth_provider_id = "slack"
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Custom Slack Message
|
||||
@@ -74,7 +74,7 @@ slackme npm run long-build
|
||||
```tf
|
||||
module "slackme" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/slackme/coder"
|
||||
source = "registry.coder.com/coder/slackme/coder"
|
||||
version = "1.0.2"
|
||||
agent_id = coder_agent.example.id
|
||||
auth_provider_id = "slack"
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, github]
|
||||
tags: [hashicorp, integration, vault, github]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (GitHub)
|
||||
@@ -15,7 +15,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-github/coder"
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
@@ -47,7 +47,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-github/coder"
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
@@ -60,7 +60,7 @@ module "vault" {
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-github/coder"
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
@@ -74,7 +74,7 @@ module "vault" {
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-github/coder"
|
||||
source = "registry.coder.com/coder/vault-github/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
|
||||
@@ -5,21 +5,22 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, jwt, oidc]
|
||||
tags: [hashicorp, integration, vault, jwt, oidc]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (JWT)
|
||||
|
||||
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
|
||||
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/users/oidc-auth) access token from Coder's OIDC authentication method or another source of jwt token. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = "eyJhbGciOiJIUzI1N..." # optional, if not present, defaults to user's oidc authentication token
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,8 +43,8 @@ curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/d
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_auth_path = "oidc"
|
||||
@@ -58,8 +59,8 @@ data "coder_workspace_owner" "me" {}
|
||||
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
|
||||
@@ -71,11 +72,114 @@ module "vault" {
|
||||
```tf
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||
version = "1.0.20"
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.0.31"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_cli_version = "1.17.5"
|
||||
}
|
||||
```
|
||||
|
||||
### Use a custom JWT token
|
||||
|
||||
```tf
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
jwt = {
|
||||
source = "geektheripper/jwt"
|
||||
version = "1.1.4"
|
||||
}
|
||||
time = {
|
||||
source = "hashicorp/time"
|
||||
version = "0.11.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "jwt_signed_token" "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
algorithm = "RS256"
|
||||
# `openssl genrsa -out key.pem 4096` and `openssl rsa -in key.pem -pubout > pub.pem` to generate keys
|
||||
key = file("key.pem")
|
||||
claims_json = jsonencode({
|
||||
iss = "https://code.example.com"
|
||||
sub = "${data.coder_workspace.me.id}"
|
||||
aud = "https://vault.example.com"
|
||||
iat = provider::time::rfc3339_parse(plantimestamp()).unix
|
||||
# Uncomment to set an expiry on the JWT token(default 3600 seconds).
|
||||
# workspace will need to be restarted to generate a new token if it expires
|
||||
#exp = provider::time::rfc3339_parse(timeadd(timestamp(), 3600)).unix agent = coder_agent.main.id
|
||||
provisioner = data.coder_provisioner.main.id
|
||||
provisioner_arch = data.coder_provisioner.main.arch
|
||||
provisioner_os = data.coder_provisioner.main.os
|
||||
|
||||
workspace = data.coder_workspace.me.id
|
||||
workspace_url = data.coder_workspace.me.access_url
|
||||
workspace_port = data.coder_workspace.me.access_port
|
||||
workspace_name = data.coder_workspace.me.name
|
||||
template = data.coder_workspace.me.template_id
|
||||
template_name = data.coder_workspace.me.template_name
|
||||
template_version = data.coder_workspace.me.template_version
|
||||
owner = data.coder_workspace_owner.me.id
|
||||
owner_name = data.coder_workspace_owner.me.name
|
||||
owner_email = data.coder_workspace_owner.me.email
|
||||
owner_login_type = data.coder_workspace_owner.me.login_type
|
||||
owner_groups = data.coder_workspace_owner.me.groups
|
||||
})
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vault-jwt/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||
vault_jwt_token = jwt_signed_token.vault[0].token
|
||||
}
|
||||
```
|
||||
|
||||
#### Example Vault JWT role
|
||||
|
||||
```shell
|
||||
vault write auth/JWT_MOUNT/role/workspace - << EOF
|
||||
{
|
||||
"user_claim": "sub",
|
||||
"bound_audiences": "https://vault.example.com",
|
||||
"role_type": "jwt",
|
||||
"ttl": "1h",
|
||||
"claim_mappings": {
|
||||
"owner": "owner",
|
||||
"owner_email": "owner_email",
|
||||
"owner_login_type": "owner_login_type",
|
||||
"owner_name": "owner_name",
|
||||
"provisioner": "provisioner",
|
||||
"provisioner_arch": "provisioner_arch",
|
||||
"provisioner_os": "provisioner_os",
|
||||
"sub": "sub",
|
||||
"template": "template",
|
||||
"template_name": "template_name",
|
||||
"template_version": "template_version",
|
||||
"workspace": "workspace",
|
||||
"workspace_name": "workspace_name",
|
||||
"workspace_id": "workspace_id"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example workspace access Vault policy
|
||||
|
||||
```tf
|
||||
path "kv/data/app/coder/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.owner_name}}/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.workspace_name}}" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "subscribe"]
|
||||
subscribe_event_types = ["*"]
|
||||
}
|
||||
path "kv/metadata/app/coder/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.owner_name}}/{{identity.entity.aliases.<MOUNT_ACCESSOR>.metadata.workspace_name}}" {
|
||||
capabilities = ["create", "read", "update", "delete", "list", "subscribe"]
|
||||
subscribe_event_types = ["*"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -20,6 +20,13 @@ variable "vault_addr" {
|
||||
description = "The address of the Vault server."
|
||||
}
|
||||
|
||||
variable "vault_jwt_token" {
|
||||
type = string
|
||||
description = "The JWT token used for authentication with Vault."
|
||||
default = null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "vault_jwt_auth_path" {
|
||||
type = string
|
||||
description = "The path to the Vault JWT auth method."
|
||||
@@ -46,7 +53,7 @@ resource "coder_script" "vault" {
|
||||
display_name = "Vault (GitHub)"
|
||||
icon = "/icon/vault.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token,
|
||||
CODER_OIDC_ACCESS_TOKEN : var.vault_jwt_token != null ? var.vault_jwt_token : data.coder_workspace_owner.me.oidc_access_token,
|
||||
VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path,
|
||||
VAULT_JWT_ROLE : var.vault_jwt_role,
|
||||
VAULT_CLI_VERSION : var.vault_cli_version,
|
||||
|
||||
@@ -9,11 +9,11 @@ CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN}
|
||||
fetch() {
|
||||
dest="$1"
|
||||
url="$2"
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -O "$${dest}" "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
elif command -v busybox >/dev/null 2>&1; then
|
||||
busybox wget -O "$${dest}" "$${url}"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
@@ -22,9 +22,9 @@ fetch() {
|
||||
}
|
||||
|
||||
unzip_safe() {
|
||||
if command -v unzip > /dev/null 2>&1; then
|
||||
if command -v unzip >/dev/null 2>&1; then
|
||||
command unzip "$@"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
elif command -v busybox >/dev/null 2>&1; then
|
||||
busybox unzip "$@"
|
||||
else
|
||||
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
|
||||
@@ -56,7 +56,7 @@ install() {
|
||||
|
||||
# Check if the vault CLI is installed and has the correct version
|
||||
installation_needed=1
|
||||
if command -v vault > /dev/null 2>&1; then
|
||||
if command -v vault >/dev/null 2>&1; then
|
||||
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||
@@ -81,7 +81,7 @@ install() {
|
||||
return 1
|
||||
fi
|
||||
rm vault.zip
|
||||
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||
if sudo mv vault /usr/local/bin/vault 2>/dev/null; then
|
||||
printf "Vault installed successfully!\n\n"
|
||||
else
|
||||
mkdir -p ~/.local/bin
|
||||
@@ -107,6 +107,6 @@ rm -rf "$TMP"
|
||||
|
||||
# Authenticate with Vault
|
||||
printf "🔑 Authenticating with Vault ...\n\n"
|
||||
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=-
|
||||
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write -field=token auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=- | vault login -
|
||||
printf "🥳 Vault authentication complete!\n\n"
|
||||
printf "You can now use Vault CLI to access secrets.\n"
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, token]
|
||||
tags: [hashicorp, integration, vault, token]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (Token)
|
||||
@@ -20,10 +20,10 @@ variable "vault_token" {
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/vault-token/coder"
|
||||
version = "1.0.7"
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_token = var.token
|
||||
vault_token = var.token # optional
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
```
|
||||
@@ -73,11 +73,11 @@ variable "vault_token" {
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/vault-token/coder"
|
||||
version = "1.0.7"
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.token
|
||||
vault_cli_version = "1.15.0"
|
||||
vault_cli_version = "1.19.0"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -7,6 +7,5 @@ describe("vault-token", async () => {
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
vault_addr: "foo",
|
||||
vault_token: "foo",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ variable "vault_token" {
|
||||
type = string
|
||||
description = "The Vault token to use for authentication."
|
||||
sensitive = true
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "vault_cli_version" {
|
||||
@@ -56,6 +57,7 @@ resource "coder_env" "vault_addr" {
|
||||
}
|
||||
|
||||
resource "coder_env" "vault_token" {
|
||||
count = var.vault_token != null ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "VAULT_TOKEN"
|
||||
value = var.vault_token
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch VS Code Desktop
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, vscode, helper]
|
||||
tags: [ide, vscode]
|
||||
---
|
||||
|
||||
# VS Code Desktop
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
```tf
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
@@ -29,7 +29,7 @@ module "vscode" {
|
||||
```tf
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: VS Code Web - Visual Studio Code in the browser
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, ide, vscode, web]
|
||||
tags: [ide, vscode, web]
|
||||
---
|
||||
|
||||
# VS Code Web
|
||||
@@ -14,8 +14,8 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@@ -30,8 +30,8 @@ module "vscode-web" {
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@@ -44,8 +44,8 @@ module "vscode-web" {
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
@@ -59,8 +59,8 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -77,8 +77,8 @@ By default, this module installs the latest. To pin a specific version, retrieve
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/vscode-web/coder"
|
||||
version = "1.0.30"
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
|
||||
@@ -136,6 +136,16 @@ variable "subdomain" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "platform" {
|
||||
type = string
|
||||
description = "The platform to use for the VS Code Web."
|
||||
default = ""
|
||||
validation {
|
||||
condition = var.platform == "" || var.platform == "linux" || var.platform == "darwin" || var.platform == "alpine" || var.platform == "win32"
|
||||
error_message = "Incorrect value. Please set either 'linux', 'darwin', or 'alpine' or 'win32'."
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
@@ -158,6 +168,7 @@ resource "coder_script" "vscode-web" {
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
SERVER_BASE_PATH : local.server_base_path,
|
||||
COMMIT_ID : var.commit_id,
|
||||
PLATFORM : var.platform,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
|
||||
@@ -59,15 +59,26 @@ case "$ARCH" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect the platform
|
||||
if [ -n "${PLATFORM}" ]; then
|
||||
DETECTED_PLATFORM="${PLATFORM}"
|
||||
elif [ -f /etc/alpine-release ] || grep -qi 'ID=alpine' /etc/os-release 2>/dev/null || command -v apk > /dev/null 2>&1; then
|
||||
DETECTED_PLATFORM="alpine"
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
DETECTED_PLATFORM="darwin"
|
||||
else
|
||||
DETECTED_PLATFORM="linux"
|
||||
fi
|
||||
|
||||
# Check if a specific VS Code Web commit ID was provided
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
HASH="${COMMIT_ID}"
|
||||
else
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2)
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-$DETECTED_PLATFORM-$ARCH-web | cut -d '"' -f 2)
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web commit id version $HASH.\n"
|
||||
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-$DETECTED_PLATFORM-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Microsoft Visual Studio Code Server: $output"
|
||||
|
||||
@@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
||||
# AWS example. See below for examples of using this module with other providers
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
@@ -33,7 +33,7 @@ module "windows_rdp" {
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
@@ -45,7 +45,7 @@ module "windows_rdp" {
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
display_name: Windsurf Editor
|
||||
description: Add a one-click button to launch Windsurf Editor
|
||||
icon: ../../../../.icons/windsurf.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, windsurf, ai]
|
||||
---
|
||||
|
||||
# Windsurf Editor
|
||||
|
||||
Add a button to open any workspace with a single click in Windsurf Editor.
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("windsurf", 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.windsurf_url.value).toBe(
|
||||
"windsurf://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.windsurf_url.value).toBe(
|
||||
"windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: true,
|
||||
});
|
||||
expect(state.outputs.windsurf_url.value).toBe(
|
||||
"windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: false,
|
||||
});
|
||||
expect(state.outputs.windsurf_url.value).toBe(
|
||||
"windsurf://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: true,
|
||||
});
|
||||
expect(state.outputs.windsurf_url.value).toBe(
|
||||
"windsurf://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: 22,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Cursor IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "windsurf" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/windsurf.svg"
|
||||
slug = "windsurf"
|
||||
display_name = "Windsurf Editor"
|
||||
order = var.order
|
||||
url = join("", [
|
||||
"windsurf://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
}
|
||||
|
||||
output "windsurf_url" {
|
||||
value = coder_app.windsurf.url
|
||||
description = "Windsurf Editor URL."
|
||||
}
|
||||
|
Before Width: | Height: | Size: 603 KiB After Width: | Height: | Size: 603 KiB |
@@ -3,5 +3,5 @@ display_name: Nataindata
|
||||
bio: Data engineer
|
||||
github: nataindata
|
||||
website: https://www.nataindata.com
|
||||
status: partner
|
||||
status: community
|
||||
---
|
||||
|
||||
@@ -3,9 +3,8 @@ display_name: airflow
|
||||
description: A module that adds Apache Airflow in your Coder template
|
||||
icon: ../../../../.icons/airflow.svg
|
||||
maintainer_github: nataindata
|
||||
partner_github: coder
|
||||
verified: true
|
||||
tags: [airflow, idea, web, helper]
|
||||
tags: [airflow, ide, web]
|
||||
---
|
||||
|
||||
# airflow
|
||||
@@ -15,8 +14,8 @@ A module that adds Apache Airflow in your Coder template.
|
||||
```tf
|
||||
module "airflow" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/apache-airflow/coder"
|
||||
version = "1.0.13"
|
||||
source = "registry.coder.com/nataindata/apache-airflow/coder"
|
||||
version = "1.0.14"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,18 +4,18 @@ description: Install Node.js via nvm
|
||||
icon: ../../../../.icons/node.svg
|
||||
maintainer_github: TheZoker
|
||||
verified: false
|
||||
tags: [helper]
|
||||
tags: [helper, nodejs]
|
||||
---
|
||||
|
||||
# nodejs
|
||||
|
||||
Automatically installs [Node.js](https://github.com/nodejs/node) via [nvm](https://github.com/nvm-sh/nvm). It can also install multiple versions of node and set a default version. If no options are specified, the latest version is installed.
|
||||
Automatically installs [Node.js](https://github.com/nodejs/node) via [`nvm`](https://github.com/nvm-sh/nvm). It can also install multiple versions of node and set a default version. If no options are specified, the latest version is installed.
|
||||
|
||||
```tf
|
||||
module "nodejs" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/nodejs/coder"
|
||||
version = "1.0.10"
|
||||
source = "registry.coder.com/thezoker/nodejs/coder"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -27,8 +27,8 @@ This installs multiple versions of Node.js:
|
||||
```tf
|
||||
module "nodejs" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/nodejs/coder"
|
||||
version = "1.0.10"
|
||||
source = "registry.coder.com/thezoker/nodejs/coder"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
node_versions = [
|
||||
"18",
|
||||
@@ -46,8 +46,8 @@ A example with all available options:
|
||||
```tf
|
||||
module "nodejs" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/nodejs/coder"
|
||||
version = "1.0.10"
|
||||
source = "registry.coder.com/thezoker/nodejs/coder"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
nvm_version = "v0.39.7"
|
||||
nvm_install_prefix = "/opt/nvm"
|
||||
|
||||
@@ -17,8 +17,8 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
source = "registry.coder.com/whizus/exoscale-instance-type/coder"
|
||||
version = "1.0.13"
|
||||
default = "standard.medium"
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ Change the display name a type using the corresponding maps:
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
source = "registry.coder.com/whizus/exoscale-instance-type/coder"
|
||||
version = "1.0.13"
|
||||
default = "standard.medium"
|
||||
|
||||
custom_names = {
|
||||
@@ -81,8 +81,8 @@ Show only gpu1 types
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
source = "registry.coder.com/whizus/exoscale-instance-type/coder"
|
||||
version = "1.0.13"
|
||||
default = "gpu.large"
|
||||
type_category = ["gpu"]
|
||||
exclude = [
|
||||
|
||||
@@ -23,13 +23,13 @@ describe("exoscale-instance-type", async () => {
|
||||
expect(state.outputs.value.value).toBe("gpu3.huge");
|
||||
});
|
||||
|
||||
it("fails because of wrong categroy definition", async () => {
|
||||
it("fails when default value is provided without any matching category definitions", 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');
|
||||
}).toThrow(/value "gpu3.huge" must be defined as one of options/);
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
|
||||
@@ -17,8 +17,8 @@ Customize the preselected parameter value:
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
source = "registry.coder.com/whizus/exoscale-zone/coder"
|
||||
version = "1.0.13"
|
||||
default = "ch-dk-2"
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ Change the display name and icon for a zone using the corresponding maps:
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
source = "registry.coder.com/whizus/exoscale-zone/coder"
|
||||
version = "1.0.13"
|
||||
default = "at-vie-1"
|
||||
|
||||
custom_names = {
|
||||
@@ -77,8 +77,9 @@ Hide the Switzerland zones Geneva and Zurich
|
||||
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/whizus/exoscale-zone/coder"
|
||||
version = "1.0.13"
|
||||
exclude = ["ch-gva-2", "ch-dk-2"]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This scripts creates a new sample moduledir with required files
|
||||
# Run it like : ./scripts/new_module.sh my-namespace/my-module
|
||||
|
||||
MODULE_ARG=$1
|
||||
|
||||
# Check if they are in the root directory
|
||||
if [ ! -d "registry" ]; then
|
||||
echo "Please run this script from the root directory of the repository"
|
||||
echo "Usage: ./scripts/new_module.sh <namespace>/<module_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if module name is in the format <namespace>/<module_name>
|
||||
if ! [[ "$MODULE_ARG" =~ ^[a-z0-9_-]+/[a-z0-9_-]+$ ]]; then
|
||||
echo "Module name must be in the format <namespace>/<module_name>"
|
||||
echo "Usage: ./scripts/new_module.sh <namespace>/<module_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the namespace and module name
|
||||
NAMESPACE=$(echo "$MODULE_ARG" | cut -d'/' -f1)
|
||||
MODULE_NAME=$(echo "$MODULE_ARG" | cut -d'/' -f2)
|
||||
|
||||
# Check if the module already exists
|
||||
if [ -d "registry/$NAMESPACE/modules/$MODULE_NAME" ]; then
|
||||
echo "Module at registry/$NAMESPACE/modules/$MODULE_NAME already exists"
|
||||
echo "Please choose a different name"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "registry/${NAMESPACE}/modules/${MODULE_NAME}"
|
||||
|
||||
# Copy required files from the example module
|
||||
cp -r examples/modules/* "registry/${NAMESPACE}/modules/${MODULE_NAME}/"
|
||||
|
||||
# Change to module directory
|
||||
cd "registry/${NAMESPACE}/modules/${MODULE_NAME}"
|
||||
|
||||
# Detect OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" main.tf
|
||||
sed -i '' "s/MODULE_NAME/${MODULE_NAME}/g" README.md
|
||||
else
|
||||
# Linux
|
||||
sed -i "s/MODULE_NAME/${MODULE_NAME}/g" main.tf
|
||||
sed -i "s/MODULE_NAME/${MODULE_NAME}/g" README.md
|
||||
fi
|
||||
|
||||
# Make run.sh executable
|
||||
chmod +x run.sh
|
||||
@@ -30,6 +30,12 @@ export const runContainer = async (
|
||||
return containerID.trim();
|
||||
};
|
||||
|
||||
export interface scriptOutput {
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the only "coder_script" resource in the given state and runs it in a
|
||||
* container.
|
||||
@@ -38,13 +44,15 @@ export const executeScriptInContainer = async (
|
||||
state: TerraformState,
|
||||
image: string,
|
||||
shell = "sh",
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stdout: string[];
|
||||
stderr: string[];
|
||||
}> => {
|
||||
before?: string,
|
||||
): Promise<scriptOutput> => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
if (before) {
|
||||
await execContainer(id, [shell, "-c", before]);
|
||||
}
|
||||
|
||||
const resp = await execContainer(id, [shell, "-c", instance.script]);
|
||||
const stdout = resp.stdout.trim().split("\n");
|
||||
const stderr = resp.stderr.trim().split("\n");
|
||||
@@ -58,12 +66,13 @@ export const executeScriptInContainer = async (
|
||||
export const execContainer = async (
|
||||
id: string,
|
||||
cmd: string[],
|
||||
args?: string[],
|
||||
): Promise<{
|
||||
exitCode: number;
|
||||
stderr: string;
|
||||
stdout: string;
|
||||
}> => {
|
||||
const proc = spawn(["docker", "exec", id, ...cmd], {
|
||||
const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], {
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
|
||||