Compare commits

...

17 Commits

Author SHA1 Message Date
Hugo Dutka 121328f671 chore(modules/claude-code): update readme (#172)
The README points to an old version of claude code that doesn't support
the `--continue` flag, making it incompatible with the latest version of
the module. It's easy to copy and paste the example and run into an
error. This PR updates the example version and adds troubleshooting
notes.
2025-07-02 20:26:46 +02:00
Benjamin Peinhardt dbd4928706 fix: update release action to match new registry-server dev flow (#171)
## Description

This PR updates the CI release flow to match the registry-server's new
development flow.
The "dev" registry now deploys directly from the main branch of
registry-server.
The "production" registry now deploys from tagged releases of
registry-server. We have added a "production" tag that will
track the same commit as the latest semver release of the
registry-server.

---

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Related Issues

Fixes https://github.com/coder/registry/pull/170

Closes #
2025-07-02 10:39:21 -05:00
Garrett Delfosse a1cea027dc fix: disable production deployments from main (#170)
Reverts https://github.com/coder/registry/pull/168 which should not have
been merged, and disables production deployments for now until a fix is
in place on the registry-server side.
2025-07-02 10:30:47 -04:00
Hugo Dutka 9aacddef1a fix: skip deploying to the dev registry (#168)
The workflow doesn't work currently.
2025-07-02 13:08:54 +02:00
Hugo Dutka 58faf32b81 feat(modules/claude-code): make the module ready for Coder Tasks (#160)
Related to https://github.com/coder/internal/issues/700

This PR:

- makes AgentAPI a required dependency of the module. It's now used:
- to improve task reporting (by exporting `CODER_MCP_AI_AGENTAPI_URL`
before running `coder exp mcp configure claude-code`)
- to add a web chat interface to Claude (using the `Claude Code Web`
workspace app)
- removes support for tmux and screen since we don't need them if we
have AgentAPI
- makes the Claude Code CLI workspace app optional and disabled by
default - a new `experiment_cli_app` module variable controls its
presence
- makes the module spawn the `coder_ai_task` resource, which makes the
module compatible with the new Coder Tasks feature
- makes Claude Code remember the conversation between workspace restarts
using the `--continue` flag. Previously the module's implementation was
a bit bugged

Note: the filebrowser tests stopped passing because of an upstream
update in the filebrowser project around required password length. I
confirmed they are not related to this PR's changes.

---------

Co-authored-by: Ben Potter <me@bpmct.net>
2025-07-01 19:02:13 +02:00
Michael Smith 225aff06a7 chore: update all icons to use high-quality .svg files (#165)
## Description

This PR updates all of our current icons to use high-quality SVG files.
All icons use a perfect square aspect ratio to make sure they look good
on the registry.coder.com website, too.

This PR does **not** update our validation process to enforce the use of
.svg files. That may be something worth considering in the future, but
better to wait until we know for certain that we won't ever need other
image formats.

## Type of Change

- [x] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-06-26 11:48:12 -04:00
Michael Smith dd7b31d2ac chore: update all Coder logos to use new branding (#163)
## Description

This PR swaps out all instances of the old Coder logo for the logo that
is launching in about an hour. If this is the first time you've seen the
new logo, be sure to check out [coder.com](https://coder.com/) later
today – we're refreshing the whole website and brand!

## Type of Change
- [x] Other

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally
2025-06-25 08:00:10 -04:00
Atif Ali da67cd3b36 chore: update amazon-q.svg (#158) 2025-06-23 18:05:42 +05:00
DevCats 77392cc146 feat(local-windows-rdp): local Windows RDP using coder desktop (#119)
Introduces coder module: local-windows-rdp
- Creates a coder app that can launch local rdp with auto-login using
coder-desktop
- Runs a PowerShell script inside of the VM setting RDP permissions, and
sets Username and Password inside of VM


### Testing
- [x] AWS
- [x] GCP
- [ ] Azure

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-06-23 08:03:44 -05:00
Atif Ali 7a2b1ac76d chore(amamzon-q): remove variables section (#157)
## Description

We already render all available values at
https://registry.coder.com/modules/coder/amazon-q?tab=variables

---

## Type of Change

- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [x] Documentation
- [ ] Other

---
2025-06-23 08:02:09 -05:00
DevCats e5ccf74ccc feat: claude-code workspace persistence (#154)
## Description

Add Tmux Plugin Manager with resurrect and continuum plugins. Add
functionality to be able to enable workspace persistence to save the
tmux session automatically so that it can persist through workspace
restarts.

---

## Type of Change

- [ ] New module
- [ ] Bug fix
- [X] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v1.4.0`  
**Breaking change:** [ ] Yes [X] No

---

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally

---

## Related Issues

Closes [#29](https://github.com/coder/registry/issues/29)
2025-06-19 16:46:18 -05:00
DevCats a47ff911e1 fix: Version-Bump Workflow/Script - Formatting before diff (#155)
## Description

Set up Pre-Req's, and ensure that formatting is done before checking
diff since it likes to not respect prettier formatting.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-06-19 16:35:10 -05:00
Ben Potter a8e23647c5 feat: add option to disable VS Code Web workspace trust protection (#131)
for admins with certainty about what is installed in the environment,
this is ideal. otherwise, it's best to get user trust

---------

Co-authored-by: DevelopmentCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-06-16 21:24:51 -05:00
DevCats 960ec18d35 fix: clean up version-bump workflow script output handling (#153)
## Description

Removed unnecessary comments and added commands to reset the working
directory and clean untracked files in the version-bump workflow. This
improves the script's reliability by ensuring a clean state after
executing version checks.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Related Issues

None
2025-06-16 19:52:56 -05:00
DevCats eae64160bd fix: update GitHub Actions permissions in version-bump workflow (#152)
## Description

update GitHub Actions permissions in version-bump workflow by adding
issues permission for commenting on PR's

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Related Issues

None
2025-06-16 14:32:09 -05:00
Spike Curtis b58bfebcf3 fix: disable UDP connections on windows-rdp module (#149)
## Description

Relates to 

Fixes an issue where RDP doesn't function properly over Coder Connect,
by disabling UDP and relying only on TCP. c.f.
https://github.com/coder/internal/issues/608#issuecomment-2965923672 for
a detailed description of the problem.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/windows-rdp`  
**New version:** `v1.0.19`  
**Breaking change:** [ ] Yes [x] No

---

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

---

## Related Issues

https://github.com/coder/internal/issues/608

Closes #

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2025-06-13 06:18:11 +00:00
Benjamin Peinhardt 05124309ee feat: add templates and update icon paths (#144)
This PR copies the templates in coder/coder/examples/templates over to
the registry, so that template contribution can be done through the
registry.
For now, the starter templates in the coder/coder binary and the
templates available in coder/registry will simply be different
constructs, until we find a solution we like around a single source of
truth for templates that doesn't raise hairy semver concerns for
coder/coder:
https://codercom.slack.com/archives/C05T7165ET1/p1749493368773469
2025-06-12 13:06:46 -05:00
87 changed files with 7665 additions and 551 deletions
+9
View File
@@ -190,6 +190,15 @@ main() {
done <<< "$modules"
# Always run formatter to ensure consistent formatting
echo "🔧 Running formatter to ensure consistent formatting..."
if command -v bun >/dev/null 2>&1; then
bun fmt >/dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
else
echo "⚠️ Warning: bun not found, skipping formatting"
fi
echo ""
echo "📋 Summary:"
echo "Bump Type: $bump_type"
echo ""
+4 -1
View File
@@ -1,4 +1,7 @@
[default.extend-words]
muc = "muc" # For Munich location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
HashiCorp = "HashiCorp"
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
+2 -3
View File
@@ -27,7 +27,6 @@ jobs:
- 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
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
- name: Deploy to registry.coder.com
run: |
gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --branch main
run: gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --tag production
+17 -4
View File
@@ -17,6 +17,7 @@ jobs:
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -24,6 +25,17 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Install dependencies
run: bun install
- name: Extract bump type from label
id: bump-type
run: |
@@ -46,7 +58,6 @@ jobs:
- name: Check version bump requirements
id: version-check
run: |
# Run the script to check what versions should be
output_file=$(mktemp)
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
echo "Script completed successfully"
@@ -56,17 +67,14 @@ jobs:
exit 1
fi
# Store output for PR comment
{
echo "output<<EOF"
cat "$output_file"
echo "EOF"
} >> $GITHUB_OUTPUT
# Show output
cat "$output_file"
# Check if any files would be modified by the script
if git diff --quiet; then
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
echo "✅ All module versions are already up to date"
@@ -78,6 +86,10 @@ jobs:
echo ""
echo "Diff preview:"
git diff
git checkout .
git clean -fd
exit 1
fi
@@ -85,6 +97,7 @@ jobs:
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const output = `${{ steps.version-check.outputs.output }}`;
const bumpType = `${{ steps.bump-type.outputs.type }}`;
+13 -14
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+13 -268
View File
@@ -1,268 +1,13 @@
<?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>
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.2075 2.20837C63.1244 -0.736125 56.8639 -0.736125 51.7925 2.20837L14.1826 23.9766C9.09954 26.9211 5.9751 32.3544 5.9751 38.2317V81.7683C5.9751 87.6456 9.09954 93.0789 14.1826 96.0234L51.7925 117.792C56.8755 120.736 63.1361 120.736 68.2075 117.792L105.817 96.0234C110.9 93.0789 114.025 87.6456 114.025 81.7683V38.2317C114.025 32.3544 110.9 26.9211 105.817 23.9766L68.2075 2.20837Z" fill="url(#paint0_linear_292_106)"/>
<path d="M53.6113 18.0993L26.937 33.5346C22.8682 35.8832 20.3733 40.2298 20.3733 44.9387V75.8208C20.3733 80.5297 22.8798 84.8647 26.937 87.2249L53.6113 102.66C57.6801 105.009 62.6815 105.009 66.7503 102.66L93.4247 87.2249C97.4934 84.8763 99.9883 80.5297 99.9883 75.8208V44.9387C99.9883 40.2298 97.4818 35.8949 93.4247 33.5346L66.7503 18.0993C62.6815 15.7507 57.6801 15.7507 53.6113 18.0993ZM57.7151 25.4138C59.7436 24.2337 62.2502 24.2337 64.2787 25.4138L89.7056 40.1246C91.7342 41.3048 92.9933 43.4664 92.9933 45.8267V75.26C92.9933 77.6086 91.7458 79.7819 89.7056 80.962L64.2787 95.6728C62.2502 96.853 59.7436 96.853 57.7151 95.6728L32.2881 80.962C30.2596 79.7819 29.0005 77.6203 29.0005 75.26V45.8267C29.0005 43.4781 30.2479 41.3048 32.2881 40.1246L57.7151 25.4138ZM60.5947 53.0127C60.0817 52.7206 59.4638 52.7206 58.9508 53.0127L53.4831 56.1792C52.9701 56.4713 52.667 57.0205 52.667 57.6047V63.9377C52.667 64.5219 52.9818 65.0711 53.4831 65.3632L58.9508 68.5297C59.4638 68.8218 60.0817 68.8218 60.5947 68.5297L66.0624 65.3632C66.5754 65.0711 66.8785 64.5219 66.8785 63.9377V57.6047C66.8785 57.0205 66.5638 56.4713 66.0624 56.1792L60.5947 53.0127ZM64.6984 59.1237L61.4108 65.7021L92.6086 83.8014L95.8962 77.223L64.6984 59.1237Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_292_106" x1="86.3055" y1="14.2705" x2="41.9042" y2="99.5712" gradientUnits="userSpaceOnUse">
<stop stop-color="#2FABFF"/>
<stop offset="0.31" stop-color="#5570FF"/>
<stop offset="0.62" stop-color="#7B36FF"/>
<stop offset="0.81" stop-color="#6A2CDC"/>
<stop offset="1" stop-color="#5921B8"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_293_450)">
<path d="M58.5288 0.34381L5.96707 27.4409C4.81829 28.1161 4.48575 29.1238 4.49583 30.5849L4.5966 32.4089L60.0907 60.8664L115.454 32.8623C115.454 32.8623 115.514 32.5802 115.514 31.5926C115.514 31.5926 115.464 30.2927 115.172 29.6881C114.849 29.0028 114.164 28.3881 113.721 28.0153C112.542 27.0378 111.363 26.1712 109.357 25.0426C80.6075 8.78835 61.7031 0.414348 61.7031 0.414348C60.4837 -0.0391172 59.476 -0.200349 58.5288 0.34381Z" fill="#DEA66C"/>
<path d="M5.32223 92.3671C8.38565 94.3926 13.9784 97.0529 19.6316 100.116C32.772 107.231 49.3789 115.877 57.6421 119.273C58.7606 119.736 59.4257 119.958 60.2017 119.998C60.7761 120.029 61.7233 119.646 61.7233 119.646C61.7233 119.646 60.1815 64.0306 60.0908 60.8563C60.0001 57.7526 58.7002 56.7247 57.5514 55.979C55.7577 54.8302 51.868 53.0365 51.868 53.0365C51.868 53.0365 38.2438 46.2648 24.6399 39.1302C18.1704 35.7343 12.7087 32.2577 6.83378 29.3958C5.75554 28.8819 4.48584 29.5269 4.48584 30.7361V90.9967C4.48584 91.571 4.83854 92.0547 5.32223 92.3671Z" fill="#B38251"/>
<path d="M113.993 92.7198L61.834 119.615C60.9976 120.049 60 119.444 60 118.497L60.0907 60.8765C60.0907 60.3222 59.9194 59.1029 59.7481 58.6192C59.2745 57.2084 60.3023 56.6441 60.7659 56.4123L112.501 29.7687C113.872 29.0633 115.514 30.0609 115.514 31.6027V90.2409C115.514 91.2788 114.93 92.2361 113.993 92.7198Z" fill="#966239"/>
<path opacity="0.5" d="M90.6947 43.4231C90.6947 43.4231 62.1062 58.0448 61.6427 59.2843C61.1791 60.5137 84.0741 49.5096 86.7546 48.129C89.7878 46.5671 113.61 33.8297 113.61 32.429C113.62 31.2802 90.6947 43.4231 90.6947 43.4231Z" fill="#212121"/>
<g opacity="0.5">
<path d="M8.41577 89.0316C12.1342 90.906 15.8224 92.8408 20.4175 95.1887C20.4175 94.4732 20.4175 94.1105 20.4175 93.395C15.8425 91.0571 12.1443 89.1324 8.41577 87.2379C8.41577 87.9534 8.41577 88.3162 8.41577 89.0316Z" fill="#212121"/>
<path d="M16.2456 89.8479C17.2936 90.382 17.8176 90.654 18.8052 91.1579C18.8152 88.034 18.8152 86.4721 18.8253 83.3482C17.8378 82.8443 17.3238 82.5823 16.2859 82.0483C16.2658 85.1721 16.2557 86.724 16.2456 89.8479Z" fill="#212121"/>
<path d="M9.73584 86.5023C10.6025 86.9457 11.0862 87.1976 12.0737 87.7015C12.0939 84.5877 12.1039 83.0358 12.1241 79.922C11.1265 79.4081 10.6428 79.1662 9.76607 78.7027C9.75599 81.8266 9.74592 83.3885 9.73584 86.5023Z" fill="#212121"/>
<path d="M14.603 82.1188C16.9913 83.3381 18.261 83.9931 20.3671 85.0613C19.3191 83.1567 18.7447 82.1893 17.5959 80.2344C16.4169 81.0003 15.8022 81.3731 14.603 82.1188Z" fill="#212121"/>
<path d="M8.41577 78.9445C10.1994 79.8716 11.3986 80.4863 13.7566 81.6956C12.5877 79.7306 12.0032 78.7531 10.9249 76.8284C9.82655 77.6245 9.31263 78.0376 8.41577 78.9445Z" fill="#212121"/>
</g>
<g opacity="0.5">
<path d="M27.5622 97.6475C28.3986 98.0808 28.8319 98.3126 29.6683 98.7459C29.6079 95.0375 29.5776 93.1834 29.5071 89.475C28.7009 89.0518 28.2979 88.8402 27.4917 88.427C27.5219 92.1152 27.532 93.9593 27.5622 97.6475Z" fill="#212121"/>
<path d="M24.4888 96.9925C27.5522 98.6048 29.9304 99.814 32.8728 101.386C32.8527 100.328 32.0365 99.0482 30.928 98.4536C29.1947 97.5366 28.2072 97.0328 26.4538 96.1158C25.3252 95.5414 24.4888 95.9545 24.4888 96.9925Z" fill="#212121"/>
<path d="M30.3233 86.5225C30.8373 87.6108 31.0993 88.165 31.6132 89.2533C31.2101 90.1099 30.9985 90.5432 30.5753 91.3897C30.938 91.3594 31.1194 91.3493 31.4822 91.3191C31.9659 90.6742 32.2077 90.3517 32.6713 89.7068C32.2481 88.7092 32.0264 88.2053 31.583 87.2077C32.8124 87.8829 33.417 88.2255 34.5355 88.8805C34.5355 92.488 32.1775 94.0802 28.6304 92.226C25.0833 90.3819 22.7354 86.2605 22.7354 82.6428C25.3755 83.9629 27.1491 84.8295 30.3233 86.5225Z" fill="#212121"/>
</g>
<path d="M97.0231 37.2761C96.5192 36.9032 42.4158 8.63719 42.4158 8.63719L25.6274 17.3135L81.1014 45.9222L81.1316 45.9524L97.3657 37.6388C97.3657 37.6388 97.3355 37.5582 97.2448 37.4575C97.1743 37.397 97.0533 37.3063 97.0231 37.2761Z" fill="#FFE0B2"/>
<path d="M97.285 37.5179C88.0948 42.0425 81.1014 45.9222 81.1014 45.9222C81.3331 46.1539 81.3432 46.4562 81.3432 46.6074C81.313 47.7763 81.2928 48.7034 81.2626 49.8623C81.1215 55.8581 81.0409 61.864 80.8394 67.8598C81.5145 67.4567 82.3308 68.3435 82.9454 67.8598C83.8927 67.1141 85.9081 63.829 86.2104 63.7282C86.6135 63.5972 87.0468 63.698 87.46 63.6779C89.2738 63.5771 93.3349 59.8788 94.2015 59.7075C95.0379 59.5463 97.799 60.0804 97.799 60.0804C97.799 60.0804 97.6075 41.7906 97.537 38.7171C97.5168 37.6288 97.285 37.5179 97.285 37.5179Z" fill="#BF9F85"/>
</g>
<defs>
<clipPath id="clip0_293_450">
<rect width="120" height="120" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="66" height="48" viewBox="0 0 66 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M64.3029 20.8302C62.9894 20.8302 62.1144 20.0449 62.1144 18.4331V9.17517C62.1144 3.26504 59.7268 0 53.5592 0H50.6941V6.24078H51.5697C53.9968 6.24078 55.1508 7.60467 55.1508 10.0431V18.2264C55.1508 21.7807 56.1853 23.2273 58.4535 23.9713C56.1853 24.6739 55.1508 26.1617 55.1508 29.716C55.1508 31.7412 55.1508 33.7663 55.1508 35.7916C55.1508 37.4861 55.1508 39.1393 54.7131 40.8337C54.2754 42.4044 53.5592 43.8922 52.5644 45.1733C52.0073 45.9174 51.3707 46.5373 50.6545 47.116V47.9425H53.5193C59.687 47.9425 62.0746 44.6774 62.0746 38.7672V29.5094C62.0746 27.8562 62.9103 27.1123 64.2634 27.1123H65.8944V20.8714H64.3029V20.8302Z" fill="#D9D9D9"/>
<path d="M44.8049 9.42443H35.9712C35.7722 9.42443 35.6131 9.25912 35.6131 9.05247V8.34987C35.6131 8.14322 35.7722 7.97791 35.9712 7.97791H44.8447C45.0436 7.97791 45.2028 8.14322 45.2028 8.34987V9.05247C45.2028 9.25912 45.0038 9.42443 44.8049 9.42443Z" fill="#D9D9D9"/>
<path d="M46.3171 18.3513H39.871C39.672 18.3513 39.5128 18.1859 39.5128 17.9792V17.2767C39.5128 17.0701 39.672 16.9047 39.871 16.9047H46.3171C46.5161 16.9047 46.6752 17.0701 46.6752 17.2767V17.9792C46.6752 18.1446 46.5161 18.3513 46.3171 18.3513Z" fill="#D9D9D9"/>
<path d="M48.8636 13.8879H35.9712C35.7722 13.8879 35.6131 13.7226 35.6131 13.5159V12.8133C35.6131 12.6067 35.7722 12.4413 35.9712 12.4413H48.8237C49.0228 12.4413 49.182 12.6067 49.182 12.8133V13.5159C49.182 13.6812 49.0626 13.8879 48.8636 13.8879Z" fill="#D9D9D9"/>
<path d="M25.7449 11.4483C26.6203 11.4483 27.4958 11.531 28.3313 11.7377V10.0431C28.3313 7.64602 29.5251 6.24078 31.9126 6.24078H32.7879V0H29.923C23.7552 0 21.3679 3.26504 21.3679 9.17517V12.2336C22.7605 11.7377 24.2329 11.4483 25.7449 11.4483Z" fill="#D9D9D9"/>
<path d="M51.5695 33.9308C50.9329 28.6819 47.0333 24.3009 42.0196 23.3089C40.6269 23.0197 39.2342 22.9783 37.8813 23.2263C37.8415 23.2263 37.8415 23.1849 37.8018 23.1849C35.6132 18.4321 30.9179 15.291 25.8246 15.291C20.7313 15.291 16.0757 18.3494 13.8474 23.1023C13.8076 23.1023 13.8076 23.1437 13.7678 23.1437C12.3353 22.9783 10.9028 23.0609 9.47035 23.433C4.5362 24.6728 0.795835 28.9711 0.119377 34.1786C0.039787 34.7159 0 35.2532 0 35.7492C0 37.3196 1.03457 38.7662 2.54664 38.9729C4.41683 39.2623 6.04827 37.7743 6.00848 35.8732C6.00848 35.5838 6.00848 35.2532 6.04827 34.9639C6.36659 32.3188 8.31638 30.087 10.863 29.467C11.6589 29.2604 12.4547 29.2191 13.2107 29.3432C15.638 29.6738 18.0255 28.3925 19.06 26.1607C19.8161 24.5075 21.0098 23.0609 22.6015 22.2757C24.3522 21.4077 26.3418 21.2838 28.1723 21.9452C30.0822 22.6477 31.5146 24.1355 32.3901 25.9953C33.3053 27.814 33.743 29.0951 35.6928 29.3432C36.4886 29.467 38.7169 29.4257 39.5526 29.3844C41.184 29.3844 42.8154 29.963 43.9694 31.1616C44.7254 31.9881 45.2825 33.0214 45.5213 34.1786C45.8793 36.0385 45.4417 37.8983 44.3673 39.3035C43.6112 40.2954 42.5767 41.0394 41.4227 41.37C40.8656 41.5354 40.3085 41.5766 39.7514 41.5766C39.4332 41.5766 38.9955 41.5766 38.4782 41.5766C36.8866 41.5766 33.5043 41.5766 30.9576 41.5766C29.2069 41.5766 27.8141 40.1302 27.8141 38.3116V26.2019C27.8141 25.7061 27.4162 25.2928 26.9387 25.2928H25.7052C23.2778 25.334 21.3281 28.1446 21.3281 31.1202C21.3281 34.096 21.3281 41.99 21.3281 41.99C21.3281 45.2137 23.8349 47.8175 26.9387 47.8175C26.9387 47.8175 40.7464 47.7761 40.9452 47.7761C44.1285 47.4454 47.0731 45.751 49.0626 43.1472C51.0522 40.6261 51.9674 37.3196 51.5695 33.9308Z" fill="#D9D9D9"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" fill="white"/>
<path d="M57.933 54C75.2624 54.0001 84.9775 62.7841 85.3057 75.7138L70.3392 76.2054C69.9453 69.0379 64.0048 64.3297 57.933 64.4701C49.5965 64.6458 43.4257 70.5838 43.4256 79.9999C43.4256 89.4162 49.5964 95.2491 57.933 95.2491C64.0048 95.2485 69.8139 90.7514 70.4704 83.5838L85.4368 83.9354C85.043 97.0757 74.7372 106 57.933 106C41.1286 106 28 95.8108 28 79.9999C28.0001 64.1189 40.6035 54 57.933 54ZM132 55.5364V104.726H92.6151V55.5364H132Z" fill="#090B0B"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_293_107)">
<path d="M60.285 96.2158V119.197C99.2373 119.197 129.566 81.5665 116.824 40.7488C111.256 22.9113 97.0819 8.73763 79.2444 3.16935C38.4267 -9.5728 0.795288 20.7566 0.795288 59.7089H23.8294C23.8328 59.7089 23.8355 59.7029 23.8355 59.7029C23.8388 35.2575 48.0312 16.3581 73.7321 25.684C83.2536 29.1394 90.8485 36.7329 94.3059 46.2537C103.633 71.9395 84.7622 96.1224 60.3376 96.1571V73.2498L37.3703 73.2485L37.3662 96.2158H60.285ZM37.3622 113.866H19.7183L19.7142 96.2158H37.3662L37.3622 113.866ZM19.7264 96.2158H4.93541C4.92934 96.2158 4.92461 96.2117 4.92461 96.2117V81.4275C4.92461 81.4275 4.92934 81.4167 4.93541 81.4167H19.7156C19.7216 81.4167 19.7264 81.4215 19.7264 81.4215V96.2158Z" fill="#0080FF"/>
</g>
<defs>
<clipPath id="clip0_293_107">
<rect width="120" height="120" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 953 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M118.049 51.0329C115.107 49.0467 107.377 48.1984 101.756 49.7167C101.453 44.1017 98.5669 39.37 93.2862 35.2415L91.3313 33.9253L90.0286 35.8988C87.4676 39.7981 86.3886 44.993 86.771 49.7151C87.0725 52.6245 88.0816 55.8951 90.0286 58.2681C82.7137 62.524 75.9715 61.5579 46.112 61.5579H0.0103499C-0.124524 68.3204 0.95923 81.3298 9.20719 91.92C10.118 93.0898 11.1176 94.2214 12.2014 95.3116C18.907 102.047 29.0385 106.986 44.1888 107C67.3015 107.021 87.1042 94.4904 99.1493 64.1919C103.113 64.2572 113.576 64.9049 118.697 54.98C118.822 54.8129 119.999 52.3475 119.999 52.3475L118.048 51.0313L118.049 51.0329ZM30.0968 44.8481H17.133V57.8511H30.0968V44.8481ZM46.845 44.8481H33.8812V57.8511H46.845V44.8481ZM63.5932 44.8481H50.6294V57.8511H63.5932V44.8481ZM80.3414 44.8481H67.3777V57.8511H80.3414V44.8481ZM13.3486 44.8481H0.384824V57.8511H13.3486V44.8481ZM30.0968 28.4249H17.133V41.4279H30.0968V28.4249ZM46.845 28.4249H33.8812V41.4279H46.845V28.4249ZM63.5932 28.4249H50.6294V41.4279H63.5932V28.4249ZM63.5932 12H50.6294V25.003H63.5932V12Z" fill="#1D63ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+14 -90
View File
@@ -1,90 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns" width="44" height="51" viewBox="0 0 44 51" version="2.0">
<title>Group.svg</title>
<desc>Created using Figma 0.90</desc>
<g id="Canvas" transform="translate(-1640 -2453)" figma:type="canvas">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path9 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path0_fill" transform="translate(1640.54 2474.36)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path10 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path1_fill" transform="translate(1645.68 2474.37)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path11 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path2_fill" transform="translate(1653.39 2474.26)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path12 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path3_fill" transform="translate(1660.43 2474.39)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path13 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path4_fill" transform="translate(1667.55 2472.54)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path14 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path5_fill" transform="translate(1672.47 2474.29)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path15 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path6_fill" transform="translate(1679.98 2474.24)" fill="#4E4E4E" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
<g id="g" style="mix-blend-mode:normal;" figma:type="group">
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path16 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path7_fill" transform="translate(1673.48 2453.69)" fill="#767677" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path17 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path8_fill" transform="translate(1643.21 2484.27)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path18 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path9_fill" transform="translate(1643.21 2457.88)" fill="#F37726" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path19 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path10_fill" transform="translate(1643.28 2496.09)" fill="#9E9E9E" style="mix-blend-mode:normal;"/>
</g>
</g>
<g id="path" style="mix-blend-mode:normal;" figma:type="group">
<g id="path20 fill" style="mix-blend-mode:normal;" figma:type="vector">
<use xlink:href="#path11_fill" transform="translate(1641.87 2458.43)" fill="#616262" style="mix-blend-mode:normal;"/>
</g>
</g>
</g>
</g>
</g>
</g>
<defs>
<path id="path0_fill" d="M 1.74498 5.47533C 1.74498 7.03335 1.62034 7.54082 1.29983 7.91474C 0.943119 8.23595 0.480024 8.41358 0 8.41331L 0.124642 9.3036C 0.86884 9.31366 1.59095 9.05078 2.15452 8.56466C 2.45775 8.19487 2.6834 7.76781 2.818 7.30893C 2.95261 6.85005 2.99341 6.36876 2.93798 5.89377L 2.93798 0L 1.74498 0L 1.74498 5.43972L 1.74498 5.47533Z"/>
<path id="path1_fill" d="M 5.50204 4.76309C 5.50204 5.43081 5.50204 6.02731 5.55545 6.54368L 4.496 6.54368L 4.42478 5.48423C 4.20318 5.85909 3.88627 6.16858 3.50628 6.38125C 3.12628 6.59392 2.69675 6.70219 2.26135 6.69503C 1.22861 6.69503 0 6.13415 0 3.84608L 0 0.0445149L 1.193 0.0445149L 1.193 3.6057C 1.193 4.84322 1.57583 5.67119 2.65309 5.67119C 2.87472 5.67358 3.09459 5.63168 3.29982 5.54796C 3.50505 5.46424 3.69149 5.34039 3.84822 5.18366C 4.00494 5.02694 4.1288 4.84049 4.21252 4.63527C 4.29623 4.43004 4.33813 4.21016 4.33575 3.98853L 4.33575 0L 5.52874 0L 5.52874 4.72748L 5.50204 4.76309Z"/>
<path id="path2_fill" d="M 0.0534178 2.27264C 0.0534178 1.44466 0.0534178 0.768036 0 0.153731L 1.06836 0.153731L 1.12177 1.2666C 1.3598 0.864535 1.70247 0.534594 2.11325 0.311954C 2.52404 0.0893145 2.98754 -0.0176786 3.45435 0.00238095C 5.03908 0.00238095 6.23208 1.32892 6.23208 3.30538C 6.23208 5.63796 4.7987 6.79535 3.24958 6.79535C 2.85309 6.81304 2.45874 6.7281 2.10469 6.54874C 1.75064 6.36937 1.44888 6.10166 1.22861 5.77151L 1.22861 5.77151L 1.22861 9.33269L 0.0534178 9.33269L 0.0534178 2.29935L 0.0534178 2.27264ZM 1.22861 4.00872C 1.23184 4.17026 1.24972 4.33117 1.28203 4.48948C 1.38304 4.88479 1.61299 5.23513 1.93548 5.48506C 2.25798 5.735 2.65461 5.87026 3.06262 5.86944C 4.31794 5.86944 5.05689 4.8456 5.05689 3.3588C 5.05689 2.05897 4.36246 0.946096 3.10714 0.946096C 2.61036 0.986777 2.14548 1.20726 1.79965 1.5662C 1.45382 1.92514 1.25079 2.3979 1.22861 2.89585L 1.22861 4.00872Z"/>
<path id="path3_fill" d="M 1.31764 0.0178059L 2.75102 3.85499C 2.90237 4.28233 3.06262 4.7987 3.16946 5.18153C 3.2941 4.7898 3.42764 4.29123 3.5879 3.82828L 4.88773 0.0178059L 6.14305 0.0178059L 4.36246 4.64735C 3.47216 6.87309 2.92908 8.02158 2.11 8.71601C 1.69745 9.09283 1.19448 9.35658 0.649917 9.48166L 0.356119 8.48453C 0.736886 8.35942 1.09038 8.16304 1.39777 7.90584C 1.8321 7.55188 2.17678 7.10044 2.4038 6.5882C 2.45239 6.49949 2.48551 6.40314 2.50173 6.3033C 2.49161 6.19586 2.46457 6.0907 2.42161 5.9917L 0 0L 1.29983 0L 1.31764 0.0178059Z"/>
<path id="path4_fill" d="M 2.19013 0L 2.19013 1.86962L 3.8995 1.86962L 3.8995 2.75992L 2.19013 2.75992L 2.19013 6.26769C 2.19013 7.06896 2.42161 7.53191 3.08043 7.53191C 3.31442 7.53574 3.54789 7.5088 3.77486 7.45179L 3.82828 8.34208C 3.48794 8.45999 3.12881 8.51431 2.76882 8.50234C 2.53042 8.51726 2.29161 8.48043 2.06878 8.39437C 1.84595 8.30831 1.64438 8.17506 1.47789 8.00377C 1.11525 7.51873 0.949826 6.91431 1.01494 6.31221L 1.01494 2.75102L 0 2.75102L 0 1.86072L 1.03274 1.86072L 1.03274 0.275992L 2.19013 0Z"/>
<path id="path5_fill" d="M 1.17716 3.57899C 1.153 3.88093 1.19468 4.18451 1.29933 4.46876C 1.40398 4.75301 1.5691 5.01114 1.78329 5.22532C 1.99747 5.43951 2.2556 5.60463 2.53985 5.70928C 2.8241 5.81393 3.12768 5.85561 3.42962 5.83145C 4.04033 5.84511 4.64706 5.72983 5.21021 5.49313L 5.41498 6.38343C 4.72393 6.66809 3.98085 6.80458 3.23375 6.78406C 2.79821 6.81388 2.36138 6.74914 1.95322 6.59427C 1.54505 6.43941 1.17522 6.19809 0.869071 5.88688C 0.562928 5.57566 0.327723 5.2019 0.179591 4.79125C 0.0314584 4.38059 -0.0260962 3.94276 0.0108748 3.50777C 0.0108748 1.54912 1.17716 0 3.0824 0C 5.21911 0 5.75329 1.86962 5.75329 3.06262C 5.76471 3.24644 5.76471 3.43079 5.75329 3.61461L 1.15046 3.61461L 1.17716 3.57899ZM 4.66713 2.6887C 4.70149 2.45067 4.68443 2.20805 4.61709 1.97718C 4.54976 1.74631 4.43372 1.53255 4.2768 1.35031C 4.11987 1.16808 3.92571 1.0216 3.70739 0.920744C 3.48907 0.81989 3.25166 0.767006 3.01118 0.765656C 2.52201 0.801064 2.06371 1.01788 1.72609 1.37362C 1.38847 1.72935 1.19588 2.19835 1.18607 2.6887L 4.66713 2.6887Z"/>
<path id="path6_fill" d="M 0.0534178 2.19228C 0.0534178 1.42663 0.0534178 0.767806 0 0.162404L 1.06836 0.162404L 1.06836 1.43553L 1.12177 1.43553C 1.23391 1.04259 1.4656 0.694314 1.78468 0.439049C 2.10376 0.183783 2.4944 0.034196 2.90237 0.0110538C 3.01466 -0.00368459 3.12839 -0.00368459 3.24068 0.0110538L 3.24068 1.12393C 3.10462 1.10817 2.9672 1.10817 2.83114 1.12393C 2.427 1.13958 2.04237 1.30182 1.7491 1.58035C 1.45583 1.85887 1.27398 2.23462 1.23751 2.63743C 1.20422 2.8196 1.18635 3.00425 1.1841 3.18941L 1.1841 6.65267L 0.00890297 6.65267L 0.00890297 2.20118L 0.0534178 2.19228Z"/>
<path id="path7_fill" d="M 6.03059 2.83565C 6.06715 3.43376 5.92485 4.02921 5.6218 4.54615C 5.31875 5.0631 4.86869 5.47813 4.32893 5.73839C 3.78917 5.99864 3.18416 6.09233 2.59097 6.00753C 1.99778 5.92272 1.44326 5.66326 0.998048 5.26219C 0.552837 4.86113 0.23709 4.33661 0.0910307 3.75546C -0.0550287 3.17431 -0.0247891 2.56283 0.177897 1.99893C 0.380583 1.43503 0.746541 0.944221 1.22915 0.589037C 1.71176 0.233853 2.28918 0.0303686 2.88784 0.00450543C 3.28035 -0.0170932 3.67326 0.0391144 4.04396 0.169896C 4.41467 0.300677 4.75587 0.503453 5.04794 0.766561C 5.34 1.02967 5.57718 1.34792 5.74582 1.70301C 5.91446 2.0581 6.01124 2.44303 6.03059 2.83565L 6.03059 2.83565Z"/>
<path id="path8_fill" d="M 18.6962 7.12238C 10.6836 7.12238 3.64131 4.24672 0 0C 1.41284 3.82041 3.96215 7.1163 7.30479 9.44404C 10.6474 11.7718 14.623 13.0196 18.6962 13.0196C 22.7695 13.0196 26.745 11.7718 30.0877 9.44404C 33.4303 7.1163 35.9796 3.82041 37.3925 4.0486e-13C 33.7601 4.24672 26.7445 7.12238 18.6962 7.12238Z"/>
<path id="path9_fill" d="M 18.6962 5.89725C 26.7089 5.89725 33.7512 8.77291 37.3925 13.0196C 35.9796 9.19922 33.4303 5.90333 30.0877 3.57559C 26.745 1.24785 22.7695 4.0486e-13 18.6962 0C 14.623 4.0486e-13 10.6474 1.24785 7.30479 3.57559C 3.96215 5.90333 1.41284 9.19922 0 13.0196C 3.64131 8.76401 10.648 5.89725 18.6962 5.89725Z"/>
<path id="path10_fill" d="M 7.59576 3.56656C 7.64276 4.31992 7.46442 5.07022 7.08347 5.72186C 6.70251 6.3735 6.13619 6.89698 5.45666 7.22561C 4.77713 7.55424 4.01515 7.67314 3.26781 7.56716C 2.52046 7.46117 1.82158 7.13511 1.26021 6.63051C 0.698839 6.12591 0.300394 5.46561 0.115637 4.73375C -0.0691191 4.00188 -0.0318219 3.23159 0.222777 2.52099C 0.477376 1.8104 0.93775 1.19169 1.54524 0.743685C 2.15274 0.295678 2.87985 0.0386595 3.63394 0.00537589C 4.12793 -0.0210471 4.62229 0.0501173 5.08878 0.214803C 5.55526 0.37949 5.98473 0.63447 6.35264 0.965179C 6.72055 1.29589 7.01971 1.69584 7.233 2.1422C 7.4463 2.58855 7.56957 3.07256 7.59576 3.56656L 7.59576 3.56656Z"/>
<path id="path11_fill" d="M 2.25061 4.37943C 1.81886 4.39135 1.39322 4.27535 1.02722 4.04602C 0.661224 3.81668 0.371206 3.48424 0.193641 3.09052C 0.0160762 2.69679 -0.0411078 2.25935 0.0292804 1.83321C 0.0996686 1.40707 0.294486 1.01125 0.589233 0.695542C 0.883981 0.37983 1.2655 0.158316 1.68581 0.0588577C 2.10611 -0.0406005 2.54644 -0.0135622 2.95143 0.136572C 3.35641 0.286707 3.70796 0.553234 3.96186 0.902636C 4.21577 1.25204 4.3607 1.66872 4.37842 2.10027C 4.39529 2.6838 4.18131 3.25044 3.78293 3.67715C 3.38455 4.10387 2.83392 4.35623 2.25061 4.37943Z"/>
</defs>
</svg>
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.401 63.1422C13.401 66.8082 13.1064 68.0022 12.3488 68.882C11.5056 69.6378 10.4111 70.0558 9.27646 70.0551L9.57107 72.1499C11.3301 72.1736 13.0369 71.555 14.369 70.4112C15.0857 69.5411 15.619 68.5363 15.9372 67.4566C16.2554 66.3769 16.3518 65.2444 16.2208 64.1268V50.2591H13.401V63.0584V63.1422Z" fill="white"/>
<path d="M34.4304 61.4899C34.4304 63.061 34.4304 64.4646 34.5567 65.6795H32.0525L31.8842 63.1867C31.3604 64.0687 30.6113 64.797 29.7132 65.2974C28.815 65.7978 27.7997 66.0525 26.7706 66.0357C24.3296 66.0357 21.4256 64.7159 21.4256 59.3323V50.3874H24.2454V58.7667C24.2454 61.6785 25.1503 63.6266 27.6965 63.6266C28.2204 63.6323 28.7401 63.5337 29.2252 63.3367C29.7103 63.1397 30.1509 62.8483 30.5214 62.4795C30.8918 62.1107 31.1846 61.672 31.3825 61.1892C31.5803 60.7063 31.6794 60.1889 31.6737 59.6674V50.2827H34.4935V61.4061L34.4304 61.4899Z" fill="white"/>
<path d="M39.7754 55.3709C39.7754 53.4228 39.7754 51.8307 39.6491 50.3853H42.1743L42.3006 53.0038C42.8632 52.0578 43.6731 51.2814 44.6441 50.7576C45.615 50.2337 46.7106 49.982 47.8139 50.0292C51.5597 50.0292 54.3795 53.1504 54.3795 57.8009C54.3795 63.2893 50.9915 66.0126 47.3299 66.0126C46.3928 66.0542 45.4607 65.8544 44.6238 65.4324C43.787 65.0103 43.0737 64.3804 42.5531 63.6036V71.9828H39.7754V55.4338V55.3709ZM42.5531 59.4558C42.5607 59.8359 42.603 60.2145 42.6794 60.587C42.9181 61.5172 43.4616 62.3415 44.2239 62.9296C44.9862 63.5177 45.9236 63.8359 46.888 63.834C49.8551 63.834 51.6018 61.425 51.6018 57.9266C51.6018 54.8682 49.9604 52.2497 46.9933 52.2497C45.8191 52.3454 44.7202 52.8642 43.9028 53.7087C43.0854 54.5533 42.6055 55.6657 42.5531 56.8373V59.4558Z" fill="white"/>
<path d="M59.4037 50.3711L62.7917 59.3997C63.1494 60.4052 63.5282 61.6202 63.7807 62.521C64.0753 61.5993 64.3909 60.4262 64.7697 59.3369L67.8421 50.3711H70.8092L66.6005 61.2641C64.4962 66.5011 63.2125 69.2035 61.2765 70.8374C60.3014 71.7241 59.1126 72.3446 57.8254 72.6389L57.131 70.2928C58.031 69.9984 58.8665 69.5363 59.5931 68.9311C60.6197 68.0983 61.4344 67.0361 61.971 65.8308C62.0858 65.6221 62.1641 65.3954 62.2024 65.1605C62.1785 64.9077 62.1146 64.6602 62.0131 64.4273L56.2892 50.3292H59.3616L59.4037 50.3711Z" fill="white"/>
<path d="M78.295 45.9766V50.3757H82.3353V52.4705H78.295V60.7241C78.295 62.6094 78.8421 63.6987 80.3993 63.6987C80.9524 63.7077 81.5042 63.6443 82.0407 63.5102L82.1669 65.605C81.3625 65.8824 80.5137 66.0102 79.6628 65.9821C79.0993 66.0172 78.5348 65.9305 78.0081 65.728C77.4814 65.5255 77.005 65.212 76.6115 64.809C75.7543 63.6677 75.3633 62.2455 75.5172 60.8288V52.4496H73.1183V50.3547H75.5593V46.626L78.295 45.9766Z" fill="white"/>
<path d="M87.5296 58.5154C87.4725 59.2258 87.571 59.9401 87.8183 60.609C88.0657 61.2778 88.456 61.8852 88.9622 62.3891C89.4685 62.8931 90.0786 63.2816 90.7505 63.5278C91.4223 63.7741 92.1399 63.8721 92.8536 63.8153C94.2971 63.8474 95.7312 63.5762 97.0622 63.0193L97.5462 65.1141C95.9128 65.7839 94.1565 66.105 92.3906 66.0567C91.3611 66.1269 90.3286 65.9746 89.3639 65.6102C88.3991 65.2458 87.525 64.678 86.8014 63.9457C86.0777 63.2134 85.5218 62.334 85.1717 61.3678C84.8215 60.4015 84.6855 59.3713 84.7729 58.3478C84.7729 53.7392 87.5296 50.0942 92.0329 50.0942C97.0833 50.0942 98.3459 54.4933 98.3459 57.3004C98.3729 57.7329 98.3729 58.1667 98.3459 58.5992H87.4665L87.5296 58.5154ZM95.7786 56.4206C95.8598 55.8605 95.8195 55.2897 95.6603 54.7464C95.5012 54.2032 95.2269 53.7002 94.856 53.2714C94.4851 52.8427 94.0261 52.498 93.5101 52.2607C92.9941 52.0234 92.4329 51.899 91.8645 51.8958C90.7083 51.9791 89.6251 52.4893 88.827 53.3263C88.029 54.1633 87.5738 55.2668 87.5506 56.4206H95.7786Z" fill="white"/>
<path d="M102.624 55.1347C102.624 53.3332 102.624 51.783 102.498 50.3586H105.023V53.3542H105.15C105.415 52.4296 105.962 51.6101 106.717 51.0095C107.471 50.4089 108.394 50.0569 109.358 50.0024C109.624 49.9678 109.893 49.9678 110.158 50.0024V52.621C109.836 52.5839 109.512 52.5839 109.19 52.621C108.235 52.6578 107.326 53.0395 106.632 53.6949C105.939 54.3503 105.509 55.2344 105.423 56.1822C105.345 56.6108 105.302 57.0453 105.297 57.4809V65.6298H102.519V55.1557L102.624 55.1347Z" fill="white"/>
<path d="M101.389 8.2955C101.475 9.70282 101.139 11.1039 100.422 12.3202C99.7061 13.5366 98.6423 14.5131 97.3665 15.1255C96.0907 15.7378 94.6607 15.9583 93.2586 15.7587C91.8565 15.5592 90.5459 14.9487 89.4935 14.005C88.4412 13.0613 87.6949 11.8272 87.3497 10.4598C87.0045 9.09235 87.0759 7.65357 87.555 6.32675C88.0341 4.99992 88.8991 3.84508 90.0398 3.00935C91.1805 2.17362 92.5453 1.69484 93.9603 1.63398C94.8881 1.58316 95.8168 1.71542 96.693 2.02314C97.5692 2.33086 98.3757 2.80798 99.066 3.42706C99.7563 4.04614 100.317 4.79496 100.716 5.63047C101.114 6.46597 101.343 7.37169 101.389 8.2955Z" fill="#767677"/>
<path d="M59.7782 90.3351C40.8393 90.3351 24.1939 83.5688 15.5872 73.5765C18.9266 82.5657 24.9523 90.3208 32.8531 95.7978C40.7538 101.275 50.1506 104.211 59.7782 104.211C69.406 104.211 78.8026 101.275 86.7036 95.7978C94.6043 90.3208 100.63 82.5657 103.969 73.5765C95.3838 83.5688 78.8015 90.3351 59.7782 90.3351Z" fill="#F37726"/>
<path d="M59.7782 25.3579C78.7173 25.3579 95.3628 32.1242 103.969 42.1164C100.63 33.1273 94.6043 25.3722 86.7036 19.8952C78.8026 14.4182 69.406 11.4821 59.7782 11.4821C50.1506 11.4821 40.7538 14.4182 32.8531 19.8952C24.9523 25.3722 18.9266 33.1273 15.5872 42.1164C24.1939 32.1033 40.7552 25.3579 59.7782 25.3579Z" fill="#F37726"/>
<path d="M33.7064 109.78C33.8175 111.553 33.396 113.318 32.4955 114.852C31.5951 116.385 30.2565 117.617 28.6503 118.39C27.0442 119.163 25.2431 119.443 23.4767 119.194C21.7102 118.944 20.0583 118.177 18.7315 116.99C17.4046 115.802 16.4628 114.249 16.0261 112.527C15.5894 110.805 15.6776 108.992 16.2793 107.32C16.8811 105.648 17.9693 104.192 19.4052 103.138C20.8411 102.084 22.5597 101.479 24.3421 101.401C25.5097 101.339 26.6782 101.506 27.7808 101.894C28.8834 102.281 29.8985 102.881 30.7681 103.659C31.6377 104.438 32.3448 105.379 32.849 106.429C33.3531 107.479 33.6445 108.618 33.7064 109.78Z" fill="#9E9E9E"/>
<path d="M17.7396 23.0808C16.7191 23.1089 15.713 22.836 14.848 22.2964C13.9829 21.7567 13.2974 20.9745 12.8777 20.0481C12.458 19.1217 12.3228 18.0924 12.4892 17.0897C12.6556 16.0871 13.116 15.1557 13.8127 14.4129C14.5094 13.67 15.4112 13.1488 16.4046 12.9148C17.3981 12.6808 18.4388 12.7444 19.3961 13.0977C20.3533 13.4509 21.1843 14.078 21.7844 14.9002C22.3845 15.7223 22.7271 16.7027 22.769 17.7181C22.8089 19.0911 22.3031 20.4244 21.3615 21.4284C20.4198 22.4325 19.1183 23.0263 17.7396 23.0808Z" fill="#616262"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg version="1.1" baseProfile="full" width="340" height="310" xmlns="http://www.w3.org/2000/svg">
<g stroke="white" stroke-width="1.5">
<g transform="rotate(30) skewX(30)">
<rect x="110" y="-72" width="175" height="75" fill="#333333" transform="skewX(-50)"/>
<rect x="110" y="3" width="87.5" height="75" fill="#CDCDCD" transform="skewX(-50)"/>
<rect x="16.5" y="78.9" width="87.5" height="25" fill="#CDCDCD" />
<rect x="16.5" y="104.5" width="175" height="25" fill="#888888" />
<rect x="16.5" y="130" width="175" height="50" fill="#DD4814" />
<rect x="104" y="166" width="89.5" height="25" fill="#CDCDCD" transform="skewY(-40)"/>
<rect x="228.3" y="29.5" width="87.5" height="75" fill="#888888" transform="skewX(-50)"/>
<rect x="191.8" y="266" width="89.5" height="25" fill="#888888" transform="skewY(-40)"/>
<rect x="192" y="291" width="179.5" height="50" fill="#DD4814" transform="skewY(-40)"/>
<rect x="282.3" y="240" width="89.1" height="50" fill="#333333" transform="skewY(-40)"/>
<rect x="194" y="3.7" width="87.5" height="25" fill="#333333" />
</g>
<line x1="93" y1="57" x2="93" y2="88" />
<line x1="169" y1="131" x2="92" y2="88" />
<line x1="92" y1="88" x2="14" y2="128" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#06D092" d="M8 0L1 4v8l7 4 7-4V4L8 0zm3.119 8.797L9.254 9.863 7.001 8.65v2.549l-2.118 1.33v-5.33l1.68-1.018 2.332 1.216V4.794l2.23-1.322-.006 5.325z"/></svg>

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="160" fill="white"/>
<path d="M57.933 54C75.2624 54.0001 84.9775 62.7841 85.3057 75.7138L70.3392 76.2054C69.9453 69.0379 64.0048 64.3297 57.933 64.4701C49.5965 64.6458 43.4257 70.5838 43.4256 79.9999C43.4256 89.4162 49.5964 95.2491 57.933 95.2491C64.0048 95.2485 69.8139 90.7514 70.4704 83.5838L85.4368 83.9354C85.043 97.0757 74.7372 106 57.933 106C41.1286 106 28 95.8108 28 79.9999C28.0001 64.1189 40.6035 54 57.933 54ZM132 55.5364V104.726H92.6151V55.5364H132Z" fill="#090B0B"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

+1 -1
View File
@@ -2,7 +2,7 @@
display_name: Coder
bio: Coder provisions cloud development environments via Terraform, supporting Linux, macOS, Windows, X86, ARM, Kubernetes and more.
github: coder
avatar: ./.images/avatar.png
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/company/coderhq
website: https://www.coder.com
status: official
-19
View File
@@ -114,25 +114,6 @@ module "amazon-q" {
}
```
## 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.
+9 -9
View File
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "2.0.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@@ -30,7 +30,6 @@ module "claude-code" {
## Prerequisites
- Node.js and npm must be installed in your workspace to install Claude Code
- 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.
@@ -48,8 +47,6 @@ 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 either `screen` or `tmux` installed to use this.
```tf
variable "anthropic_api_key" {
type = string
@@ -88,26 +85,25 @@ resource "coder_agent" "main" {
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "2.0.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
claude_code_version = "0.2.57"
claude_code_version = "1.0.40"
# Enable experimental features
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
experiment_report_tasks = true
}
```
## Run standalone
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI.
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it without any task reporting to the Coder UI.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "2.0.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@@ -117,3 +113,7 @@ module "claude-code" {
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
}
```
## Troubleshooting
The module will create log files in the workspace's `~/.claude-module` directory. If you run into any issues, look at them for more information.
@@ -0,0 +1,322 @@
import {
test,
afterEach,
expect,
describe,
setDefaultTimeout,
beforeAll,
} from "bun:test";
import path from "path";
import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
writeCoder,
writeFileContainer,
} from "~test";
let cleanupFunctions: (() => Promise<void>)[] = [];
const registerCleanup = (cleanup: () => Promise<void>) => {
cleanupFunctions.push(cleanup);
};
// Cleanup logic depends on the fact that bun's built-in test runner
// runs tests sequentially.
// https://bun.sh/docs/test/discovery#execution-order
// Weird things would happen if tried to run tests in parallel.
// One test could clean up resources that another test was still using.
afterEach(async () => {
// reverse the cleanup functions so that they are run in the correct order
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
cleanupFunctions = [];
for (const cleanup of cleanupFnsCopy) {
try {
await cleanup();
} catch (error) {
console.error("Error during cleanup:", error);
}
}
});
const setupContainer = async ({
image,
vars,
}: {
image?: string;
vars?: Record<string, string>;
} = {}) => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
...vars,
});
const coderScript = findResourceInstance(state, "coder_script");
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
registerCleanup(() => removeContainer(id));
return { id, coderScript };
};
const loadTestFile = async (...relativePath: string[]) => {
return await Bun.file(
path.join(import.meta.dir, "testdata", ...relativePath),
).text();
};
const writeExecutable = async ({
containerId,
filePath,
content,
}: {
containerId: string;
filePath: string;
content: string;
}) => {
await writeFileContainer(containerId, filePath, content, {
user: "root",
});
await execContainer(
containerId,
["bash", "-c", `chmod 755 ${filePath}`],
["--user", "root"],
);
};
const writeAgentAPIMockControl = async ({
containerId,
content,
}: {
containerId: string;
content: string;
}) => {
await writeFileContainer(containerId, "/tmp/agentapi-mock.control", content, {
user: "coder",
});
};
interface SetupProps {
skipAgentAPIMock?: boolean;
skipClaudeMock?: boolean;
}
const projectDir = "/home/coder/project";
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
const { id, coderScript } = await setupContainer({
vars: {
experiment_report_tasks: "true",
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
install_claude_code: "false",
agentapi_version: "preview",
folder: projectDir,
},
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
// the module script assumes that there is a coder executable in the PATH
await writeCoder(id, await loadTestFile("coder-mock.js"));
if (!props?.skipAgentAPIMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/agentapi",
content: await loadTestFile("agentapi-mock.js"),
});
}
if (!props?.skipClaudeMock) {
await writeExecutable({
containerId: id,
filePath: "/usr/bin/claude",
content: await loadTestFile("claude-mock.js"),
});
}
await writeExecutable({
containerId: id,
filePath: "/home/coder/script.sh",
content: coderScript.script,
});
return { id };
};
const expectAgentAPIStarted = async (id: string) => {
const resp = await execContainer(id, [
"bash",
"-c",
`curl -fs -o /dev/null "http://localhost:3284/status"`,
]);
if (resp.exitCode !== 0) {
console.log("agentapi not started");
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
};
const execModuleScript = async (id: string) => {
const resp = await execContainer(id, [
"bash",
"-c",
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
return resp;
};
// increase the default timeout to 60 seconds
setDefaultTimeout(60 * 1000);
// we don't run these tests in CI because they take too long and make network
// calls. they are dedicated for local development.
describe("claude-code", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});
// test that the script runs successfully if claude starts without any errors
test("happy-path", async () => {
const { id } = await setup();
const resp = await execContainer(id, [
"bash",
"-c",
"sudo /home/coder/script.sh",
]);
expect(resp.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
// test that the script removes lastSessionId from the .claude.json file
test("last-session-id-removed", async () => {
const { id } = await setup();
await writeFileContainer(
id,
"/home/coder/.claude.json",
JSON.stringify({
projects: {
[projectDir]: {
lastSessionId: "123",
},
},
}),
);
const catResp = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude.json",
]);
expect(catResp.exitCode).toBe(0);
expect(catResp.stdout).toContain("lastSessionId");
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const catResp2 = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude.json",
]);
expect(catResp2.exitCode).toBe(0);
expect(catResp2.stdout).not.toContain("lastSessionId");
});
// test that the script handles a .claude.json file that doesn't contain
// a lastSessionId field
test("last-session-id-not-found", async () => {
const { id } = await setup();
await writeFileContainer(
id,
"/home/coder/.claude.json",
JSON.stringify({
projects: {
"/home/coder": {},
},
}),
);
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const catResp = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/.claude-module/agentapi-start.log",
]);
expect(catResp.exitCode).toBe(0);
expect(catResp.stdout).toContain(
"No lastSessionId found in .claude.json - nothing to do",
);
});
// test that if claude fails to run with the --continue flag and returns a
// no conversation found error, then the module script retries without the flag
test("no-conversation-found", async () => {
const { id } = await setup();
await writeAgentAPIMockControl({
containerId: id,
content: "no-conversation-found",
});
// check that mocking works
const respAgentAPI = await execContainer(id, [
"bash",
"-c",
"agentapi --continue",
]);
expect(respAgentAPI.exitCode).toBe(1);
expect(respAgentAPI.stderr).toContain("No conversation found to continue");
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
});
test("install-agentapi", async () => {
const { id } = await setup({ skipAgentAPIMock: true });
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
await expectAgentAPIStarted(id);
const respAgentAPI = await execContainer(id, [
"bash",
"-c",
"agentapi --version",
]);
expect(respAgentAPI.exitCode).toBe(0);
});
// the coder binary should be executed with specific env vars
// that are set by the module script
test("coder-env-vars", async () => {
const { id } = await setup();
const respModuleScript = await execModuleScript(id);
expect(respModuleScript.exitCode).toBe(0);
const respCoderMock = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/coder-mock-output.json",
]);
if (respCoderMock.exitCode !== 0) {
console.log(respCoderMock.stdout);
console.log(respCoderMock.stderr);
}
expect(respCoderMock.exitCode).toBe(0);
expect(JSON.parse(respCoderMock.stdout)).toEqual({
statusSlug: "ccw",
agentApiUrl: "http://localhost:3284",
});
});
});
+149 -113
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.7"
}
}
}
@@ -54,16 +54,22 @@ variable "claude_code_version" {
default = "latest"
}
variable "experiment_use_screen" {
variable "experiment_cli_app" {
type = bool
description = "Whether to use screen for running Claude Code in the background."
description = "Whether to create the CLI workspace app."
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_cli_app_order" {
type = number
description = "The order of the CLI workspace app."
default = null
}
variable "experiment_cli_app_group" {
type = string
description = "The group of the CLI workspace app."
default = null
}
variable "experiment_report_tasks" {
@@ -84,9 +90,29 @@ variable "experiment_post_install_script" {
default = null
}
variable "install_agentapi" {
type = bool
description = "Whether to install AgentAPI."
default = true
}
variable "agentapi_version" {
type = string
description = "The version of AgentAPI to install."
default = "v0.2.2"
}
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 have to trim the slash because otherwise coder exp mcp will
# set up an invalid claude config
workdir = trimsuffix(var.folder, "/")
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) : ""
agentapi_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-start.sh"))
agentapi_wait_for_start_script_b64 = base64encode(file("${path.module}/scripts/agentapi-wait-for-start.sh"))
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.js"))
claude_code_app_slug = "ccw"
}
# Install and Initialize Claude Code
@@ -97,23 +123,18 @@ resource "coder_script" "claude_code" {
script = <<-EOT
#!/bin/bash
set -e
set -x
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if the specified folder exists
if [ ! -d "${var.folder}" ]; then
echo "Warning: The specified folder '${var.folder}' does not exist."
if [ ! -d "${local.workdir}" ]; then
echo "Warning: The specified folder '${local.workdir}' does not exist."
echo "Creating the folder..."
# The folder must exist before tmux is started or else claude will start
# in the home directory.
mkdir -p "${var.folder}"
mkdir -p "${local.workdir}"
echo "Folder created successfully."
fi
# 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
@@ -121,22 +142,89 @@ resource "coder_script" "claude_code" {
/tmp/pre_install.sh
fi
# Install Claude Code if enabled
if [ "${var.install_claude_code}" = "true" ]; then
if ! command_exists npm; then
echo "Error: npm is not installed. Please install Node.js and npm first."
exit 1
echo "npm not found, checking for Node.js installation..."
if ! command_exists node; then
echo "Node.js not found, installing Node.js via NVM..."
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
echo "Node.js installed: $(node --version)"
echo "npm installed: $(npm --version)"
else
echo "Node.js is installed but npm is not available. Please install npm manually."
exit 1
fi
fi
echo "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
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}
if ! command_exists node; then
echo "Error: Node.js is not installed. Please install Node.js manually."
exit 1
fi
# Install AgentAPI if enabled
if [ "${var.install_agentapi}" = "true" ]; then
echo "Installing AgentAPI..."
arch=$(uname -m)
if [ "$arch" = "x86_64" ]; then
binary_name="agentapi-linux-amd64"
elif [ "$arch" = "aarch64" ]; then
binary_name="agentapi-linux-arm64"
else
echo "Error: Unsupported architecture: $arch"
exit 1
fi
curl \
--retry 5 \
--retry-delay 5 \
--fail \
--retry-all-errors \
-L \
-C - \
-o agentapi \
"https://github.com/coder/agentapi/releases/download/${var.agentapi_version}/$binary_name"
chmod +x agentapi
sudo mv agentapi /usr/local/bin/agentapi
fi
if ! command_exists agentapi; then
echo "Error: AgentAPI is not installed. Please enable install_agentapi or install it manually."
exit 1
fi
# this must be kept in sync with the agentapi-start.sh script
module_path="$HOME/.claude-module"
mkdir -p "$module_path/scripts"
# save the prompt for the agentapi start command
echo -n "$CODER_MCP_CLAUDE_TASK_PROMPT" > "$module_path/prompt.txt"
echo -n "${local.agentapi_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-start.sh"
echo -n "${local.agentapi_wait_for_start_script_b64}" | base64 -d > "$module_path/scripts/agentapi-wait-for-start.sh"
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "$module_path/scripts/remove-last-session-id.js"
chmod +x "$module_path/scripts/agentapi-start.sh"
chmod +x "$module_path/scripts/agentapi-wait-for-start.sh"
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
export CODER_MCP_APP_STATUS_SLUG="${local.claude_code_app_slug}"
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
coder exp mcp configure claude-code "${local.workdir}"
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
@@ -144,82 +232,43 @@ resource "coder_script" "claude_code" {
/tmp/post_install.sh
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."
if ! command_exists claude; then
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
exit 1
fi
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Claude Code in the background with tmux..."
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# 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 \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
fi
# Run with screen if enabled
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Claude Code in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.claude-code.log"
# Ensure the screenrc exists
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.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/.claude-code.log"
echo "multiuser on" >> "$HOME/.screenrc"
fi
if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS claude-code bash -c '
cd ${var.folder}
claude --dangerously-skip-permissions "$CODER_MCP_CLAUDE_TASK_PROMPT" | tee -a "$HOME/.claude-code.log"
exec bash
'
else
# Check if claude is installed before running
if ! command_exists claude; then
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
exit 1
fi
fi
cd "${local.workdir}"
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
"$module_path/scripts/agentapi-wait-for-start.sh"
EOT
run_on_start = true
}
resource "coder_app" "claude_code_web" {
# use a short slug to mitigate https://github.com/coder/coder/issues/15178
slug = local.claude_code_app_slug
display_name = "Claude Code Web"
agent_id = var.agent_id
url = "http://localhost:3284/"
icon = var.icon
order = var.order
group = var.group
subdomain = true
healthcheck {
url = "http://localhost:3284/status"
interval = 3
threshold = 20
}
}
resource "coder_app" "claude_code" {
count = var.experiment_cli_app ? 1 : 0
slug = "claude-code"
display_name = "Claude Code"
display_name = "Claude Code CLI"
agent_id = var.agent_id
command = <<-EOT
#!/bin/bash
@@ -228,28 +277,15 @@ resource "coder_app" "claude_code" {
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
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 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}
claude
fi
agentapi attach
EOT
icon = var.icon
order = var.order
group = var.group
order = var.experiment_cli_app_order
group = var.experiment_cli_app_group
}
resource "coder_ai_task" "claude_code" {
sidebar_app {
id = coder_app.claude_code_web.id
}
}
@@ -0,0 +1,63 @@
#!/bin/bash
set -o errexit
set -o pipefail
# this must be kept in sync with the main.tf file
module_path="$HOME/.claude-module"
scripts_dir="$module_path/scripts"
log_file_path="$module_path/agentapi.log"
# if the first argument is not empty, start claude with the prompt
if [ -n "$1" ]; then
cp "$module_path/prompt.txt" /tmp/claude-code-prompt
else
rm -f /tmp/claude-code-prompt
fi
# if the log file already exists, archive it
if [ -f "$log_file_path" ]; then
mv "$log_file_path" "$log_file_path"".$(date +%s)"
fi
# see the remove-last-session-id.js script for details
# about why we need it
# avoid exiting if the script fails
node "$scripts_dir/remove-last-session-id.js" "$(pwd)" || true
# we'll be manually handling errors from this point on
set +o errexit
function start_agentapi() {
local continue_flag="$1"
local prompt_subshell='"$(cat /tmp/claude-code-prompt)"'
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
# visible in the terminal screen by default.
agentapi server --term-width 67 --term-height 1190 -- \
bash -c "claude $continue_flag --dangerously-skip-permissions $prompt_subshell" \
> "$log_file_path" 2>&1
}
echo "Starting AgentAPI..."
# attempt to start claude with the --continue flag
start_agentapi --continue
exit_code=$?
echo "First AgentAPI exit code: $exit_code"
if [ $exit_code -eq 0 ]; then
exit 0
fi
# if there was no conversation to continue, claude exited with an error.
# start claude without the --continue flag.
if grep -q "No conversation found to continue" "$log_file_path"; then
echo "AgentAPI with --continue flag failed, starting claude without it."
start_agentapi
exit_code=$?
fi
echo "Second AgentAPI exit code: $exit_code"
exit $exit_code
@@ -0,0 +1,30 @@
#!/bin/bash
set -o errexit
set -o pipefail
# This script waits for the agentapi server to start on port 3284.
# It considers the server started after 3 consecutive successful responses.
agentapi_started=false
echo "Waiting for agentapi server to start on port 3284..."
for i in $(seq 1 150); do
for j in $(seq 1 3); do
sleep 0.1
if curl -fs -o /dev/null "http://localhost:3284/status"; then
echo "agentapi response received ($j/3)"
else
echo "agentapi server not responding ($i/15)"
continue 2
fi
done
agentapi_started=true
break
done
if [ "$agentapi_started" != "true" ]; then
echo "Error: agentapi server did not start on port 3284 after 15 seconds."
exit 1
fi
echo "agentapi server started on port 3284."
@@ -0,0 +1,40 @@
// If lastSessionId is present in .claude.json, claude --continue will start a
// conversation starting from that session. The problem is that lastSessionId
// doesn't always point to the last session. The field is updated by claude only
// at the point of normal CLI exit. If Claude exits with an error, or if the user
// restarts the Coder workspace, lastSessionId will be stale, and claude --continue
// will start from an old session.
//
// If lastSessionId is missing, claude seems to accurately figure out where to
// start using the conversation history - even if the CLI previously exited with
// an error.
//
// This script removes the lastSessionId field from .claude.json.
const path = require("path")
const fs = require("fs")
const workingDirArg = process.argv[2]
if (!workingDirArg) {
console.log("No working directory provided - it must be the first argument")
process.exit(1)
}
const workingDir = path.resolve(workingDirArg)
console.log("workingDir", workingDir)
const claudeJsonPath = path.join(process.env.HOME, ".claude.json")
console.log(".claude.json path", claudeJsonPath)
if (!fs.existsSync(claudeJsonPath)) {
console.log("No .claude.json file found")
process.exit(0)
}
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, "utf8"))
if ("projects" in claudeJson && workingDir in claudeJson.projects && "lastSessionId" in claudeJson.projects[workingDir]) {
delete claudeJson.projects[workingDir].lastSessionId
fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2))
console.log("Removed lastSessionId from .claude.json")
} else {
console.log("No lastSessionId found in .claude.json - nothing to do")
}
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const http = require("http");
const fs = require("fs");
const args = process.argv.slice(2);
const port = 3284;
const controlFile = "/tmp/agentapi-mock.control";
let control = "";
if (fs.existsSync(controlFile)) {
control = fs.readFileSync(controlFile, "utf8");
}
if (
control === "no-conversation-found" &&
args.join(" ").includes("--continue")
) {
// this must match the error message in the agentapi-start.sh script
console.error("No conversation found to continue");
process.exit(1);
}
console.log(`starting server on port ${port}`);
http
.createServer(function (_request, response) {
response.writeHead(200);
response.end(
JSON.stringify({
status: "stable",
}),
);
})
.listen(port);
@@ -0,0 +1,9 @@
#!/usr/bin/env node
const main = async () => {
console.log("mocking claude");
// sleep for 30 minutes
await new Promise((resolve) => setTimeout(resolve, 30 * 60 * 1000));
};
main();
@@ -0,0 +1,14 @@
#!/usr/bin/env node
const fs = require("fs");
const statusSlugEnvVar = "CODER_MCP_APP_STATUS_SLUG";
const agentApiUrlEnvVar = "CODER_MCP_AI_AGENTAPI_URL";
fs.writeFileSync(
"/home/coder/coder-mock-output.json",
JSON.stringify({
statusSlug: process.env[statusSlugEnvVar] ?? "env var not set",
agentApiUrl: process.env[agentApiUrlEnvVar] ?? "env var not set",
}),
);
+1 -1
View File
@@ -1,7 +1,7 @@
---
display_name: Coder Login
description: Automatically logs the user into Coder on their workspace
icon: ../../../../.icons/coder-white.svg
icon: ../../../../.icons/coder.svg
maintainer_github: coder
verified: true
tags: [helper]
@@ -0,0 +1,74 @@
---
display_name: Windows RDP Desktop
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
icon: ../../../../.icons/desktop.svg
maintainer_github: coder
verified: true
supported_os: [windows]
tags: [rdp, windows, desktop, remote]
---
# Windows RDP Desktop
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
> **Note**: [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
agent_name = coder_agent.main.name
}
```
## Features
-**Standalone Solution**: Automatically configures RDP on Windows workspaces
-**One-click Access**: Launch RDP sessions directly through Coder Desktop
-**No Port Forwarding**: Uses Coder Desktop URI handling
-**Auto-configuration**: Sets up Windows firewall, services, and authentication
-**Secure**: Configurable credentials with sensitive variable handling
-**Customizable**: Display name, credentials, and UI ordering options
## What This Module Does
1. **Enables RDP** on the Windows workspace
2. **Sets the administrator password** for RDP authentication
3. **Configures Windows Firewall** to allow RDP connections
4. **Starts RDP services** automatically
5. **Creates a Coder Desktop button** for one-click access
## Examples
### Basic Usage
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
agent_name = coder_agent.main.name
}
```
### Custom display name
Specify a custom display name for the `coder_app` button:
```tf
module "rdp_desktop" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/local-windows-rdp/coder"
version = "1.0.0"
agent_id = coder_agent.windows.id
agent_name = "windows"
display_name = "Windows Desktop"
order = 1
}
```
@@ -0,0 +1,120 @@
# PowerShell script to configure RDP for Coder Desktop access
# This script enables RDP, sets the admin password, and configures necessary settings
Write-Output "[Coder RDP Setup] Starting RDP configuration..."
# Function to set the administrator password
function Set-AdminPassword {
param (
[string]$adminUsername,
[string]$adminPassword
)
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername"
try {
# Convert password to secure string
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force
# Set the password for the user
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword
# Enable the user account (in case it's disabled)
Get-LocalUser -Name $adminUsername | Enable-LocalUser
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername"
} catch {
Write-Error "[Coder RDP Setup] Failed to set password: $_"
exit 1
}
}
# Function to enable and configure RDP
function Enable-RDP {
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..."
try {
# Enable RDP
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force
# Disable Network Level Authentication (NLA) for easier access
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force
# Set security layer to RDP Security Layer
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force
Write-Output "[Coder RDP Setup] RDP enabled successfully"
} catch {
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_"
exit 1
}
}
# Function to configure Windows Firewall for RDP
function Configure-Firewall {
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..."
try {
# Enable RDP firewall rules
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
# If the above fails, try alternative method
if ($LASTEXITCODE -ne 0) {
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
}
Write-Output "[Coder RDP Setup] Firewall configured successfully"
} catch {
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_"
# Continue anyway as RDP might still work
}
}
# Function to ensure RDP service is running
function Start-RDPService {
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..."
try {
# Start the Terminal Services
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service -Name "TermService" -ErrorAction SilentlyContinue
# Start Remote Desktop Services UserMode Port Redirector
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue
Write-Output "[Coder RDP Setup] RDP services started successfully"
} catch {
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_"
# Continue anyway
}
}
# Main execution
try {
# Template variables from Terraform
$username = "${username}"
$password = "${password}"
# Validate inputs
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
Write-Error "[Coder RDP Setup] Username or password is empty"
exit 1
}
# Execute configuration steps
Set-AdminPassword -adminUsername $username -adminPassword $password
Enable-RDP
Configure-Firewall
Start-RDPService
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!"
Write-Output "[Coder RDP Setup] You can now connect using:"
Write-Output " Username: $username"
Write-Output " Password: [hidden]"
Write-Output " Port: 3389 (default)"
} catch {
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_"
exit 1
}
@@ -0,0 +1,184 @@
import { describe, expect, it } from "bun:test";
import {
type TerraformState,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
type TestVariables = Readonly<{
agent_id: string;
agent_name: string;
username?: string;
password?: string;
display_name?: string;
order?: number;
}>;
function findRdpApp(state: TerraformState) {
for (const resource of state.resources) {
const isRdpAppResource =
resource.type === "coder_app" && resource.name === "rdp_desktop";
if (!isRdpAppResource) {
continue;
}
for (const instance of resource.instances) {
if (instance.attributes.slug === "rdp-desktop") {
return instance.attributes;
}
}
}
return null;
}
function findRdpScript(state: TerraformState) {
for (const resource of state.resources) {
const isRdpScriptResource =
resource.type === "coder_script" && resource.name === "rdp_setup";
if (!isRdpScriptResource) {
continue;
}
for (const instance of resource.instances) {
if (instance.attributes.display_name === "Configure RDP") {
return instance.attributes;
}
}
}
return null;
}
describe("local-windows-rdp", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "test-agent",
});
it("should create RDP app with default values", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const app = findRdpApp(state);
// Verify the app was created
expect(app).not.toBeNull();
expect(app?.slug).toBe("rdp-desktop");
expect(app?.display_name).toBe("RDP Desktop");
expect(app?.icon).toBe("/icon/desktop.svg");
expect(app?.external).toBe(true);
// Verify the URI format
expect(app?.url).toStartWith("coder://");
expect(app?.url).toContain("/v0/open/ws/");
expect(app?.url).toContain("/agent/main/rdp");
expect(app?.url).toContain("username=Administrator");
expect(app?.url).toContain("password=coderRDP!");
});
it("should create RDP configuration script", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const script = findRdpScript(state);
// Verify the script was created
expect(script).not.toBeNull();
expect(script?.display_name).toBe("Configure RDP");
expect(script?.icon).toBe("/icon/desktop.svg");
expect(script?.run_on_start).toBe(true);
expect(script?.run_on_stop).toBe(false);
// Verify the script contains PowerShell configuration
expect(script?.script).toContain("Set-AdminPassword");
expect(script?.script).toContain("Enable-RDP");
expect(script?.script).toContain("Configure-Firewall");
expect(script?.script).toContain("Start-RDPService");
});
it("should create RDP app with custom values", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "custom-agent-id",
agent_name: "windows-agent",
username: "CustomUser",
password: "CustomPass123!",
display_name: "Custom RDP",
order: 5,
});
const app = findRdpApp(state);
// Verify custom values
expect(app?.display_name).toBe("Custom RDP");
expect(app?.order).toBe(5);
// Verify custom credentials in URI
expect(app?.url).toContain("/agent/windows-agent/rdp");
expect(app?.url).toContain("username=CustomUser");
expect(app?.url).toContain("password=CustomPass123!");
});
it("should pass custom credentials to PowerShell script", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
username: "TestAdmin",
password: "TestPassword123!",
});
const script = findRdpScript(state);
// Verify custom credentials are in the script
expect(script?.script).toContain('$username = "TestAdmin"');
expect(script?.script).toContain('$password = "TestPassword123!"');
});
it("should handle sensitive password variable", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
password: "SensitivePass123!",
});
const app = findRdpApp(state);
// Verify password is included in URI even when sensitive
expect(app?.url).toContain("password=SensitivePass123!");
});
it("should use correct default agent name", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "main",
});
const app = findRdpApp(state);
expect(app?.url).toContain("/agent/main/rdp");
});
it("should construct proper Coder URI format", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "test-agent-id",
agent_name: "test-agent",
username: "TestUser",
password: "TestPass",
});
const app = findRdpApp(state);
// Verify complete URI structure
expect(app?.url).toMatch(
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/,
);
});
});
@@ -0,0 +1,81 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "agent_name" {
type = string
description = "The name of the Coder agent."
}
variable "username" {
type = string
description = "The username for RDP authentication."
default = "Administrator"
}
variable "password" {
type = string
description = "The password for RDP authentication."
default = "coderRDP!"
sensitive = true
}
variable "display_name" {
type = string
description = "The display name for the RDP app button."
default = "RDP Desktop"
}
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 "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
locals {
# Extract server name from workspace access URL
server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0]
}
data "coder_workspace" "me" {}
resource "coder_script" "rdp_setup" {
agent_id = var.agent_id
display_name = "Configure RDP"
icon = "/icon/desktop.svg"
script = templatefile("${path.module}/configure-rdp.ps1", {
username = var.username
password = var.password
})
run_on_start = true
}
resource "coder_app" "rdp_desktop" {
agent_id = var.agent_id
slug = "rdp-desktop"
display_name = var.display_name
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}"
icon = "/icon/desktop.svg"
external = true
order = var.order
group = var.group
}
+5 -5
View File
@@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -31,7 +31,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -45,7 +45,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -78,7 +78,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
@@ -121,6 +121,12 @@ variable "use_cached" {
default = false
}
variable "disable_trust" {
type = bool
description = "Disables workspace trust protection for VS Code Web."
default = false
}
variable "extensions_dir" {
type = string
description = "Override the directory to store extensions in."
@@ -169,6 +175,7 @@ resource "coder_script" "vscode-web" {
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
DISABLE_TRUST : var.disable_trust,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
+8 -2
View File
@@ -16,10 +16,16 @@ if [ -n "${SERVER_BASE_PATH}" ]; then
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
fi
# Set disable workspace trust
DISABLE_TRUST_ARG=""
if [ "${DISABLE_TRUST}" = true ]; then
DISABLE_TRUST_ARG="--disable-workspace-trust"
fi
run_vscode_web() {
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
}
# Check if the settings file exists...
+4 -4
View File
@@ -16,7 +16,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.2.0"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
@@ -34,7 +34,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.2.0"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
@@ -46,7 +46,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.2.0"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
}
@@ -58,7 +58,7 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.2.0"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
devolutions_gateway_version = "2025.1.6" # Specify a specific version
@@ -16,6 +16,11 @@ function Configure-RDP {
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
# Enable RDP through Windows Firewall
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
# Disable UDP. It doesn't work via `coder port-forward` and is broken due to MTU issues in Coder Connect.
# Requires a restart to take effect. c.f. https://github.com/coder/internal/issues/608#issuecomment-2965923672
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' -Name "SelectTransport" -Value 1 -PropertyType DWORD -Force
Restart-Service -Name "TermService" -Force
}
function Install-DevolutionsGateway {
@@ -0,0 +1,111 @@
---
display_name: AWS EC2 (Devcontainer)
description: Provision AWS EC2 VMs with a devcontainer as Coder workspaces
icon: ../../../../.icons/aws.svg
maintainer_github: coder
verified: true
tags: [vm, linux, aws, persistent, devcontainer]
---
# Remote Development on AWS EC2 VMs using a Devcontainer
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.
![Architecture Diagram](./architecture.svg)
<!-- TODO: Add screenshot -->
## Prerequisites
### Authentication
By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).
The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.
To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.
## Required permissions / policy
The following sample policy allows Coder to create EC2 instances and modify
instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceStatus",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
## Architecture
This template provisions the following resources:
- AWS Instance
Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## Caching
To speed up your builds, you can use a container registry as a cache.
When creating the template, set the parameter `cache_repo` to a valid Docker repository in the form `host.tld/path/to/repo`.
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
> [!NOTE]
> We recommend using a registry cache with authentication enabled.
> To allow Envbuilder to authenticate with a registry cache hosted on ECR, specify an IAM instance
> profile that has read and write access to the given registry. For more information, see the
> [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html).
>
> Alternatively, you can specify the variable `cache_repo_docker_config_path`
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.
## code-server
`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com).
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

@@ -0,0 +1,15 @@
#cloud-config
cloud_final_modules:
- [scripts-user, always]
hostname: ${hostname}
users:
- name: ${linux_user}
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- "${ssh_pubkey}"
# Automatically grow the partition
growpart:
mode: auto
devices: ['/']
ignore_growroot_disabled: false
@@ -0,0 +1,37 @@
#!/bin/bash
# Install Docker
if ! command -v docker &> /dev/null
then
echo "Docker not found, installing..."
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh 2>&1 >/dev/null
usermod -aG docker ${linux_user}
newgrp docker
else
echo "Docker is already installed."
fi
# Set up Docker credentials
mkdir -p "/home/${linux_user}/.docker"
if [ -n "${docker_config_json_base64}" ]; then
# Write the Docker config JSON to disk if it is provided.
printf "%s" "${docker_config_json_base64}" | base64 -d | tee "/home/${linux_user}/.docker/config.json"
else
# Assume that we're going to use the instance IAM role to pull from the cache repo if we need to.
# Set up the ecr credential helper.
apt-get update -y && apt-get install -y amazon-ecr-credential-helper
mkdir -p .docker
printf '{"credsStore": "ecr-login"}' | tee "/home/${linux_user}/.docker/config.json"
fi
chown -R ${linux_user}:${linux_user} "/home/${linux_user}/.docker"
# Start envbuilder
sudo -u coder docker run \
--rm \
--net=host \
-h ${hostname} \
-v /home/${linux_user}/envbuilder:/workspaces \
%{ for key, value in environment ~}
-e ${key}="${value}" \
%{ endfor ~}
${builder_image}
@@ -0,0 +1,331 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
aws = {
source = "hashicorp/aws"
}
cloudinit = {
source = "hashicorp/cloudinit"
}
envbuilder = {
source = "coder/envbuilder"
}
}
}
module "aws_region" {
source = "https://registry.coder.com/modules/aws-region"
default = "us-east-1"
}
provider "aws" {
region = module.aws_region.value
}
variable "cache_repo" {
default = ""
description = "(Optional) Use a container registry as a cache to speed up builds. Example: host.tld/path/to/repo."
type = string
}
variable "cache_repo_docker_config_path" {
default = ""
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required. This will depend on your Coder setup. Example: `/home/coder/.docker/config.json`."
sensitive = true
type = string
}
variable "iam_instance_profile" {
default = ""
description = "(Optional) Name of an IAM instance profile to assign to the instance."
type = string
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance type"
description = "What instance type should your workspace use?"
default = "t3.micro"
mutable = false
option {
name = "2 vCPU, 1 GiB RAM"
value = "t3.micro"
}
option {
name = "2 vCPU, 2 GiB RAM"
value = "t3.small"
}
option {
name = "2 vCPU, 4 GiB RAM"
value = "t3.medium"
}
option {
name = "2 vCPU, 8 GiB RAM"
value = "t3.large"
}
option {
name = "4 vCPU, 16 GiB RAM"
value = "t3.xlarge"
}
option {
name = "8 vCPU, 32 GiB RAM"
value = "t3.2xlarge"
}
}
data "coder_parameter" "root_volume_size_gb" {
name = "root_volume_size_gb"
display_name = "Root Volume Size (GB)"
description = "How large should the root volume for the instance be?"
default = 30
type = "number"
mutable = true
validation {
min = 1
monotonic = "increasing"
}
}
data "coder_parameter" "fallback_image" {
default = "codercom/enterprise-base:ubuntu"
description = "This image runs if the devcontainer fails to build."
display_name = "Fallback Image"
mutable = true
name = "fallback_image"
order = 3
}
data "coder_parameter" "devcontainer_builder" {
description = <<-EOF
Image that will build the devcontainer.
Find the latest version of Envbuilder here: https://ghcr.io/coder/envbuilder
Be aware that using the `:latest` tag may expose you to breaking changes.
EOF
display_name = "Devcontainer Builder"
mutable = true
name = "devcontainer_builder"
default = "ghcr.io/coder/envbuilder:latest"
order = 4
}
data "coder_parameter" "repo_url" {
name = "repo_url"
display_name = "Repository URL"
default = "https://github.com/coder/envbuilder-starter-devcontainer"
description = "Repository URL"
mutable = true
}
data "coder_parameter" "ssh_pubkey" {
name = "ssh_pubkey"
display_name = "SSH Public Key"
default = ""
description = "(Optional) Add an SSH public key to the `coder` user's authorized_keys. Useful for troubleshooting. You may need to add a security group to the instance."
mutable = false
}
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
count = var.cache_repo_docker_config_path == "" ? 0 : 1
filename = var.cache_repo_docker_config_path
}
data "aws_iam_instance_profile" "vm_instance_profile" {
count = var.iam_instance_profile == "" ? 0 : 1
name = var.iam_instance_profile
}
# Be careful when modifying the below locals!
locals {
# TODO: provide a way to pick the availability zone.
aws_availability_zone = "${module.aws_region.value}a"
hostname = lower(data.coder_workspace.me.name)
linux_user = "coder"
# The devcontainer builder image is the image that will build the devcontainer.
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json.
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "")
# The envbuilder provider requires a key-value map of environment variables. Build this here.
envbuilder_env = {
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
# if the cache repo is enabled.
"ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value,
# The agent token is required for the agent to connect to the Coder platform.
"CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""),
# The agent URL is required for the agent to connect to the Coder platform.
"CODER_AGENT_URL" : data.coder_workspace.me.access_url,
# The agent init script is required for the agent to start up. We base64 encode it here
# to avoid quoting issues.
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh",
"ENVBUILDER_DOCKER_CONFIG_BASE64" : local.docker_config_json_base64,
# The fallback image is the image that will run if the devcontainer fails to build.
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
# The following are used to push the image to the cache repo, if defined.
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
# You can add other required environment variables here.
# See: https://github.com/coder/envbuilder/?tab=readme-ov-file#environment-variables
}
}
# Check for the presence of a prebuilt image in the cache repo
# that we can use instead.
resource "envbuilder_cached_image" "cached" {
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
builder_image = local.devcontainer_builder_image
git_url = data.coder_parameter.repo_url.value
cache_repo = var.cache_repo
extra_env = local.envbuilder_env
}
data "cloudinit_config" "user_data" {
gzip = false
base64_encode = false
boundary = "//"
part {
filename = "cloud-config.yaml"
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
hostname = local.hostname
linux_user = local.linux_user
ssh_pubkey = data.coder_parameter.ssh_pubkey.value
})
}
part {
filename = "userdata.sh"
content_type = "text/x-shellscript"
content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", {
hostname = local.hostname
linux_user = local.linux_user
# If we have a cached image, use the cached image's environment variables.
# Otherwise, just use the environment variables we've defined in locals.
environment = try(envbuilder_cached_image.cached[0].env_map, local.envbuilder_env)
# Builder image will either be the builder image parameter, or the cached image, if cache is provided.
builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value)
docker_config_json_base64 = local.docker_config_json_base64
})
}
}
# This is useful for debugging the startup script. Left here for reference.
# resource local_file "startup_script" {
# content = data.cloudinit_config.user_data.rendered
# filename = "${path.module}/user_data.txt"
# }
resource "aws_instance" "vm" {
ami = data.aws_ami.ubuntu.id
availability_zone = local.aws_availability_zone
instance_type = data.coder_parameter.instance_type.value
iam_instance_profile = try(data.aws_iam_instance_profile.vm_instance_profile[0].name, null)
root_block_device {
volume_size = data.coder_parameter.root_volume_size_gb.value
}
user_data = data.cloudinit_config.user_data.rendered
tags = {
Name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Required if you are using our example policy, see template README
Coder_Provisioned = "true"
}
lifecycle {
ignore_changes = [ami]
}
}
resource "aws_ec2_instance_state" "vm" {
instance_id = aws_instance.vm.id
state = data.coder_workspace.me.transition == "start" ? "running" : "stopped"
}
resource "coder_agent" "dev" {
count = data.coder_workspace.me.start_count
arch = "amd64"
auth = "token"
os = "linux"
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
connection_timeout = 0
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
}
resource "coder_metadata" "info" {
count = data.coder_workspace.me.start_count
resource_id = coder_agent.dev[0].id
item {
key = "ami"
value = aws_instance.vm.ami
}
item {
key = "availability_zone"
value = local.aws_availability_zone
}
item {
key = "instance_type"
value = data.coder_parameter.instance_type.value
}
item {
key = "ssh_pubkey"
value = data.coder_parameter.ssh_pubkey.value
}
item {
key = "repo_url"
value = data.coder_parameter.repo_url.value
}
item {
key = "devcontainer_builder"
value = data.coder_parameter.devcontainer_builder.value
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
}
@@ -0,0 +1,94 @@
---
display_name: AWS EC2 (Linux)
description: Provision AWS EC2 VMs as Coder workspaces
icon: ../../../../.icons/aws.svg
maintainer_github: coder
verified: true
tags: [vm, linux, aws, persistent-vm]
---
# Remote Development on AWS EC2 VMs (Linux)
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
## Prerequisites
### Authentication
By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).
The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.
To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.
## Required permissions / policy
The following sample policy allows Coder to create EC2 instances and modify
instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceStatus",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
## Architecture
This template provisions the following resources:
- AWS Instance
Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## code-server
`code-server` is installed via the `startup_script` argument in the `coder_agent`
resource block. The `coder_app` resource is defined to access `code-server` through
the dashboard UI over `localhost:13337`.
@@ -0,0 +1,8 @@
#cloud-config
cloud_final_modules:
- [scripts-user, always]
hostname: ${hostname}
users:
- name: ${linux_user}
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
@@ -0,0 +1,2 @@
#!/bin/bash
sudo -u '${linux_user}' sh -c '${init_script}'
+296
View File
@@ -0,0 +1,296 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
cloudinit = {
source = "hashicorp/cloudinit"
}
aws = {
source = "hashicorp/aws"
}
}
}
# Last updated 2023-03-14
# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort'
data "coder_parameter" "region" {
name = "region"
display_name = "Region"
description = "The region to deploy the workspace in."
default = "us-east-1"
mutable = false
option {
name = "Asia Pacific (Tokyo)"
value = "ap-northeast-1"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Asia Pacific (Seoul)"
value = "ap-northeast-2"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Asia Pacific (Osaka)"
value = "ap-northeast-3"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Asia Pacific (Mumbai)"
value = "ap-south-1"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Asia Pacific (Singapore)"
value = "ap-southeast-1"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Asia Pacific (Sydney)"
value = "ap-southeast-2"
icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Canada (Central)"
value = "ca-central-1"
icon = "/emojis/1f1e8-1f1e6.png"
}
option {
name = "EU (Frankfurt)"
value = "eu-central-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Stockholm)"
value = "eu-north-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Ireland)"
value = "eu-west-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (London)"
value = "eu-west-2"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Paris)"
value = "eu-west-3"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "South America (São Paulo)"
value = "sa-east-1"
icon = "/emojis/1f1e7-1f1f7.png"
}
option {
name = "US East (N. Virginia)"
value = "us-east-1"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US East (Ohio)"
value = "us-east-2"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US West (N. California)"
value = "us-west-1"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US West (Oregon)"
value = "us-west-2"
icon = "/emojis/1f1fa-1f1f8.png"
}
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance type"
description = "What instance type should your workspace use?"
default = "t3.micro"
mutable = false
option {
name = "2 vCPU, 1 GiB RAM"
value = "t3.micro"
}
option {
name = "2 vCPU, 2 GiB RAM"
value = "t3.small"
}
option {
name = "2 vCPU, 4 GiB RAM"
value = "t3.medium"
}
option {
name = "2 vCPU, 8 GiB RAM"
value = "t3.large"
}
option {
name = "4 vCPU, 16 GiB RAM"
value = "t3.xlarge"
}
option {
name = "8 vCPU, 32 GiB RAM"
value = "t3.2xlarge"
}
}
provider "aws" {
region = data.coder_parameter.region.value
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "coder_agent" "dev" {
count = data.coder_workspace.me.start_count
arch = "amd64"
auth = "aws-instance-identity"
os = "linux"
startup_script = <<-EOT
set -e
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
metadata {
key = "disk"
display_name = "Disk Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = "coder stat disk --path $HOME"
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
order = 1
}
# See https://registry.coder.com/modules/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
order = 2
}
locals {
hostname = lower(data.coder_workspace.me.name)
linux_user = "coder"
}
data "cloudinit_config" "user_data" {
gzip = false
base64_encode = false
boundary = "//"
part {
filename = "cloud-config.yaml"
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
hostname = local.hostname
linux_user = local.linux_user
})
}
part {
filename = "userdata.sh"
content_type = "text/x-shellscript"
content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", {
linux_user = local.linux_user
init_script = try(coder_agent.dev[0].init_script, "")
})
}
}
resource "aws_instance" "dev" {
ami = data.aws_ami.ubuntu.id
availability_zone = "${data.coder_parameter.region.value}a"
instance_type = data.coder_parameter.instance_type.value
user_data = data.cloudinit_config.user_data.rendered
tags = {
Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
# Required if you are using our example policy, see template README
Coder_Provisioned = "true"
}
lifecycle {
ignore_changes = [ami]
}
}
resource "coder_metadata" "workspace_info" {
resource_id = aws_instance.dev.id
item {
key = "region"
value = data.coder_parameter.region.value
}
item {
key = "instance type"
value = aws_instance.dev.instance_type
}
item {
key = "disk"
value = "${aws_instance.dev.root_block_device[0].volume_size} GiB"
}
}
resource "aws_ec2_instance_state" "dev" {
instance_id = aws_instance.dev.id
state = data.coder_workspace.me.transition == "start" ? "running" : "stopped"
}
@@ -0,0 +1,96 @@
---
display_name: AWS EC2 (Windows)
description: Provision AWS EC2 VMs as Coder workspaces
icon: ../../../../.icons/aws.svg
maintainer_github: coder
verified: true
tags: [vm, windows, aws]
---
# Remote Development on AWS EC2 VMs (Windows)
Provision AWS EC2 Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
### Authentication
By default, this template authenticates to AWS with using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration).
The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`.
To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template.
## Required permissions / policy
The following sample policy allows Coder to create EC2 instances and modify
instances provisioned by Coder:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypes",
"ec2:DescribeInstanceStatus",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
```
## Architecture
This template provisions the following resources:
- AWS Instance
Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## code-server
`code-server` is installed via the `startup_script` argument in the `coder_agent`
resource block. The `coder_app` resource is defined to access `code-server` through
the dashboard UI over `localhost:13337`.
@@ -0,0 +1,214 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
aws = {
source = "hashicorp/aws"
}
}
}
# Last updated 2023-03-14
# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort'
data "coder_parameter" "region" {
name = "region"
display_name = "Region"
description = "The region to deploy the workspace in."
default = "us-east-1"
mutable = false
option {
name = "Asia Pacific (Tokyo)"
value = "ap-northeast-1"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Asia Pacific (Seoul)"
value = "ap-northeast-2"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Asia Pacific (Osaka-Local)"
value = "ap-northeast-3"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Asia Pacific (Mumbai)"
value = "ap-south-1"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Asia Pacific (Singapore)"
value = "ap-southeast-1"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Asia Pacific (Sydney)"
value = "ap-southeast-2"
icon = "/emojis/1f1f0-1f1f7.png"
}
option {
name = "Canada (Central)"
value = "ca-central-1"
icon = "/emojis/1f1e8-1f1e6.png"
}
option {
name = "EU (Frankfurt)"
value = "eu-central-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Stockholm)"
value = "eu-north-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Ireland)"
value = "eu-west-1"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (London)"
value = "eu-west-2"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "EU (Paris)"
value = "eu-west-3"
icon = "/emojis/1f1ea-1f1fa.png"
}
option {
name = "South America (São Paulo)"
value = "sa-east-1"
icon = "/emojis/1f1e7-1f1f7.png"
}
option {
name = "US East (N. Virginia)"
value = "us-east-1"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US East (Ohio)"
value = "us-east-2"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US West (N. California)"
value = "us-west-1"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "US West (Oregon)"
value = "us-west-2"
icon = "/emojis/1f1fa-1f1f8.png"
}
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance type"
description = "What instance type should your workspace use?"
default = "t3.micro"
mutable = false
option {
name = "2 vCPU, 1 GiB RAM"
value = "t3.micro"
}
option {
name = "2 vCPU, 2 GiB RAM"
value = "t3.small"
}
option {
name = "2 vCPU, 4 GiB RAM"
value = "t3.medium"
}
option {
name = "2 vCPU, 8 GiB RAM"
value = "t3.large"
}
option {
name = "4 vCPU, 16 GiB RAM"
value = "t3.xlarge"
}
option {
name = "8 vCPU, 32 GiB RAM"
value = "t3.2xlarge"
}
}
provider "aws" {
region = data.coder_parameter.region.value
}
data "coder_workspace" "me" {
}
data "coder_workspace_owner" "me" {}
data "aws_ami" "windows" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["Windows_Server-2019-English-Full-Base-*"]
}
}
resource "coder_agent" "main" {
arch = "amd64"
auth = "aws-instance-identity"
os = "windows"
}
locals {
# User data is used to stop/start AWS instances. See:
# https://github.com/hashicorp/terraform-provider-aws/issues/22
user_data_start = <<EOT
<powershell>
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
${coder_agent.main.init_script}
</powershell>
<persist>true</persist>
EOT
user_data_end = <<EOT
<powershell>
shutdown /s
</powershell>
<persist>true</persist>
EOT
}
resource "aws_instance" "dev" {
ami = data.aws_ami.windows.id
availability_zone = "${data.coder_parameter.region.value}a"
instance_type = data.coder_parameter.instance_type.value
user_data = data.coder_workspace.me.transition == "start" ? local.user_data_start : local.user_data_end
tags = {
Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
# Required if you are using our example policy, see template README
Coder_Provisioned = "true"
}
lifecycle {
ignore_changes = [ami]
}
}
resource "coder_metadata" "workspace_info" {
resource_id = aws_instance.dev.id
item {
key = "region"
value = data.coder_parameter.region.value
}
item {
key = "instance type"
value = aws_instance.dev.instance_type
}
item {
key = "disk"
value = "${aws_instance.dev.root_block_device[0].volume_size} GiB"
}
}
@@ -0,0 +1,64 @@
---
display_name: Azure VM (Linux)
description: Provision Azure VMs as Coder workspaces
icon: ../../../../.icons/azure.svg
maintainer_github: coder
verified: true
tags: [vm, linux, azure]
---
# Remote Development on Azure VMs (Linux)
Provision Azure Linux VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Azure. For example, run `az login` then `az account set --subscription=<id>`
to import credentials on the system and user running coderd. For other ways to
authenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure).
## Architecture
This template provisions the following resources:
- Azure VM (ephemeral, deleted on stop)
- Managed disk (persistent, mounted to `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
> [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
### Persistent VM
> [!IMPORTANT]
> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner.
> You will have to do this installation manually as it is not included in our official images.
It is possible to make the VM persistent (instead of ephemeral) by removing the `count` attribute in the `azurerm_linux_virtual_machine` resource block as well as adding the following snippet:
```hcl
# Stop the VM
resource "null_resource" "stop_vm" {
count = data.coder_workspace.me.transition == "stop" ? 1 : 0
depends_on = [azurerm_linux_virtual_machine.main]
provisioner "local-exec" {
# Use deallocate so the VM is not charged
command = "az vm deallocate --ids ${azurerm_linux_virtual_machine.main.id}"
}
}
# Start the VM
resource "null_resource" "start" {
count = data.coder_workspace.me.transition == "start" ? 1 : 0
depends_on = [azurerm_linux_virtual_machine.main]
provisioner "local-exec" {
command = "az vm start --ids ${azurerm_linux_virtual_machine.main.id}"
}
}
```
@@ -0,0 +1,56 @@
#cloud-config
cloud_final_modules:
- [scripts-user, always]
bootcmd:
# work around https://github.com/hashicorp/terraform-provider-azurerm/issues/6117
- until [ -e /dev/disk/azure/scsi1/lun10 ]; do sleep 1; done
device_aliases:
homedir: /dev/disk/azure/scsi1/lun10
disk_setup:
homedir:
table_type: gpt
layout: true
fs_setup:
- label: coder_home
filesystem: ext4
device: homedir.1
mounts:
- ["LABEL=coder_home", "/home/${username}"]
hostname: ${hostname}
users:
- name: ${username}
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
groups: sudo
shell: /bin/bash
packages:
- git
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${init_script}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${username}
ExecStart=/opt/coder/init
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-900
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
- chown ${username}:${username} /home/${username}
- systemctl enable coder-agent
- systemctl start coder-agent
@@ -0,0 +1,325 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
azurerm = {
source = "hashicorp/azurerm"
}
cloudinit = {
source = "hashicorp/cloudinit"
}
}
}
# See https://registry.coder.com/modules/coder/azure-region
module "azure_region" {
source = "registry.coder.com/coder/azure-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
default = "eastus"
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance type"
description = "What instance type should your workspace use?"
default = "Standard_B4ms"
icon = "/icon/azure.png"
mutable = false
option {
name = "Standard_B1ms (1 vCPU, 2 GiB RAM)"
value = "Standard_B1ms"
}
option {
name = "Standard_B2ms (2 vCPU, 8 GiB RAM)"
value = "Standard_B2ms"
}
option {
name = "Standard_B4ms (4 vCPU, 16 GiB RAM)"
value = "Standard_B4ms"
}
option {
name = "Standard_B8ms (8 vCPU, 32 GiB RAM)"
value = "Standard_B8ms"
}
option {
name = "Standard_B12ms (12 vCPU, 48 GiB RAM)"
value = "Standard_B12ms"
}
option {
name = "Standard_B16ms (16 vCPU, 64 GiB RAM)"
value = "Standard_B16ms"
}
option {
name = "Standard_D2as_v5 (2 vCPU, 8 GiB RAM)"
value = "Standard_D2as_v5"
}
option {
name = "Standard_D4as_v5 (4 vCPU, 16 GiB RAM)"
value = "Standard_D4as_v5"
}
option {
name = "Standard_D8as_v5 (8 vCPU, 32 GiB RAM)"
value = "Standard_D8as_v5"
}
option {
name = "Standard_D16as_v5 (16 vCPU, 64 GiB RAM)"
value = "Standard_D16as_v5"
}
option {
name = "Standard_D32as_v5 (32 vCPU, 128 GiB RAM)"
value = "Standard_D32as_v5"
}
}
data "coder_parameter" "home_size" {
name = "home_size"
display_name = "Home volume size"
description = "How large would you like your home volume to be (in GB)?"
default = 20
type = "number"
icon = "/icon/azure.png"
mutable = false
validation {
min = 1
max = 1024
}
}
provider "azurerm" {
features {}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = "amd64"
os = "linux"
auth = "azure-instance-identity"
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = <<-EOT
#!/bin/bash
set -e
top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}'
EOT
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = <<-EOT
#!/bin/bash
set -e
free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }'
EOT
}
metadata {
key = "disk"
display_name = "Disk Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = <<-EOT
#!/bin/bash
set -e
df /home/coder | awk '$NF=="/"{printf "%s", $5}'
EOT
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
locals {
prefix = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
}
data "cloudinit_config" "user_data" {
gzip = false
base64_encode = true
boundary = "//"
part {
filename = "cloud-config.yaml"
content_type = "text/cloud-config"
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
username = "coder" # Ensure this user/group does not exist in your VM image
init_script = base64encode(coder_agent.main.init_script)
hostname = lower(data.coder_workspace.me.name)
})
}
}
resource "azurerm_resource_group" "main" {
name = "${local.prefix}-resources"
location = module.azure_region.value
tags = {
Coder_Provisioned = "true"
}
}
// Uncomment here and in the azurerm_network_interface resource to obtain a public IP
#resource "azurerm_public_ip" "main" {
# name = "publicip"
# resource_group_name = azurerm_resource_group.main.name
# location = azurerm_resource_group.main.location
# allocation_method = "Static"
#
# tags = {
# Coder_Provisioned = "true"
# }
#}
resource "azurerm_virtual_network" "main" {
name = "network"
address_space = ["10.0.0.0/24"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = {
Coder_Provisioned = "true"
}
}
resource "azurerm_subnet" "internal" {
name = "internal"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.0.0/29"]
}
resource "azurerm_network_interface" "main" {
name = "nic"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
// Uncomment for public IP address as well as azurerm_public_ip resource above
//public_ip_address_id = azurerm_public_ip.main.id
}
tags = {
Coder_Provisioned = "true"
}
}
resource "azurerm_managed_disk" "home" {
create_option = "Empty"
location = azurerm_resource_group.main.location
name = "home"
resource_group_name = azurerm_resource_group.main.name
storage_account_type = "StandardSSD_LRS"
disk_size_gb = data.coder_parameter.home_size.value
}
// azurerm requires an SSH key (or password) for an admin user or it won't start a VM. However,
// cloud-init overwrites this anyway, so we'll just use a dummy SSH key.
resource "tls_private_key" "dummy" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "azurerm_linux_virtual_machine" "main" {
count = data.coder_workspace.me.start_count
name = "vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = data.coder_parameter.instance_type.value
// cloud-init overwrites this, so the value here doesn't matter
admin_username = "adminuser"
admin_ssh_key {
public_key = tls_private_key.dummy.public_key_openssh
username = "adminuser"
}
network_interface_ids = [
azurerm_network_interface.main.id,
]
computer_name = lower(data.coder_workspace.me.name)
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-focal"
sku = "20_04-lts-gen2"
version = "latest"
}
user_data = data.cloudinit_config.user_data.rendered
tags = {
Coder_Provisioned = "true"
}
}
resource "azurerm_virtual_machine_data_disk_attachment" "home" {
count = data.coder_workspace.me.transition == "start" ? 1 : 0
managed_disk_id = azurerm_managed_disk.home.id
virtual_machine_id = azurerm_linux_virtual_machine.main[0].id
lun = "10"
caching = "ReadWrite"
}
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = azurerm_linux_virtual_machine.main[0].id
item {
key = "type"
value = azurerm_linux_virtual_machine.main[0].size
}
}
resource "coder_metadata" "home_info" {
resource_id = azurerm_managed_disk.home.id
item {
key = "size"
value = "${data.coder_parameter.home_size.value} GiB"
}
}
@@ -0,0 +1,12 @@
<FirstLogonCommands>
<SynchronousCommand>
<CommandLine>cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1"</CommandLine>
<Description>Copy Initialize.ps1 to file from CustomData</Description>
<Order>3</Order>
</SynchronousCommand>
<SynchronousCommand>
<CommandLine>powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log"</CommandLine>
<Description>Execute Initialize.ps1 script</Description>
<Order>4</Order>
</SynchronousCommand>
</FirstLogonCommands>
@@ -0,0 +1,73 @@
# This script gets run once when the VM is first created.
# Initialize the data disk & home directory.
$disk = Get-Disk -Number 2
if ($disk.PartitionStyle -Eq 'RAW')
{
"Initializing data disk"
$disk | Initialize-Disk
} else {
"data disk already initialized"
}
$partitions = Get-Partition -DiskNumber $disk.Number | Where-Object Type -Ne 'Reserved'
if ($partitions.Count -Eq 0) {
"Creating partition on data disk"
$partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize
} else {
$partition = $partitions[0]
$s = "data disk already has partition of size {0:n1} GiB" -f ($partition.Size / 1073741824)
Write-Output $s
}
$volume = Get-Volume -Partition $partition
if ($volume.FileSystemType -Eq 'Unknown')
{
"Formatting data disk"
Format-Volume -InputObject $volume -FileSystem NTFS -Confirm:$false
} else {
"data disk is already formatted"
}
# Mount the partition
Add-PartitionAccessPath -InputObject $partition -AccessPath "F:"
# Enable RDP
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
# Enable RDP through Windows Firewall
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
# Disable Network Level Authentication (NLA)
# Clients will connect via Coder's tunnel
(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -ComputerName $env:COMPUTERNAME -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(0)
# Install Chocolatey package manager
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Reload path so sessions include "choco" and "refreshenv"
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# Install Git and reload path
choco install -y git
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# Set protocol to TLS1.2 for agent download
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Set Coder Agent to run immediately, and on each restart
$init_script = @'
${init_script}
'@
Out-File -FilePath "C:\AzureData\CoderAgent.ps1" -InputObject $init_script
$task = @{
TaskName = 'CoderAgent'
Action = (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 *>> C:\AzureData\CoderAgent.log"')
Trigger = (New-ScheduledTaskTrigger -AtStartup), (New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(15))
Settings = (New-ScheduledTaskSettingsSet -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::FromDays(3650)) -Compatibility Win8)
Principal = (New-ScheduledTaskPrincipal -UserId "$env:COMPUTERNAME\$env:USERNAME" -RunLevel Highest -LogonType S4U)
}
Register-ScheduledTask @task -Force
# Additional Chocolatey package installs (optional, uncomment to enable)
# choco feature enable -n=allowGlobalConfirmation
# choco install visualstudio2022community --package-parameters "--add=Microsoft.VisualStudio.Workload.ManagedDesktop;includeRecommended --passive --locale en-US"
@@ -0,0 +1,64 @@
---
display_name: Azure VM (Windows)
description: Provision Azure VMs as Coder workspaces
icon: ../../../../.icons/azure.svg
maintainer_github: coder
verified: true
tags: [vm, windows, azure]
---
# Remote Development on Azure VMs (Windows)
Provision Azure Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Azure. For example, run `az login` then `az account set --subscription=<id>`
to import credentials on the system and user running coderd. For other ways to
authenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure).
## Architecture
This template provisions the following resources:
- Azure VM (ephemeral, deleted on stop)
- Managed disk (persistent, mounted to `F:`)
This means, when the workspace restarts, any tools or files outside of the data directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
> [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
### Persistent VM
> [!IMPORTANT]
> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner.
> You will have to do this installation manually as it is not included in our official images.
It is possible to make the VM persistent (instead of ephemeral) by removing the `count` attribute in the `azurerm_windows_virtual_machine` resource block as well as adding the following snippet:
```hcl
# Stop the VM
resource "null_resource" "stop_vm" {
count = data.coder_workspace.me.transition == "stop" ? 1 : 0
depends_on = [azurerm_windows_virtual_machine.main]
provisioner "local-exec" {
# Use deallocate so the VM is not charged
command = "az vm deallocate --ids ${azurerm_windows_virtual_machine.main.id}"
}
}
# Start the VM
resource "null_resource" "start" {
count = data.coder_workspace.me.transition == "start" ? 1 : 0
depends_on = [azurerm_windows_virtual_machine.main]
provisioner "local-exec" {
command = "az vm start --ids ${azurerm_windows_virtual_machine.main.id}"
}
}
```
@@ -0,0 +1,210 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
azurerm = {
source = "hashicorp/azurerm"
}
}
}
provider "azurerm" {
features {}
}
provider "coder" {}
data "coder_workspace" "me" {}
# See https://registry.coder.com/modules/coder/azure-region
module "azure_region" {
source = "registry.coder.com/coder/azure-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
default = "eastus"
}
# See https://registry.coder.com/modules/coder/windows-rdp
module "windows_rdp" {
source = "registry.coder.com/coder/windows-rdp/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
admin_username = local.admin_username
admin_password = random_password.admin_password.result
agent_id = resource.coder_agent.main.id
resource_id = null # Unused, to be removed in a future version
}
data "coder_parameter" "data_disk_size" {
description = "Size of your data (F:) drive in GB"
display_name = "Data disk size"
name = "data_disk_size"
default = 20
mutable = "false"
type = "number"
validation {
min = 5
max = 5000
}
}
resource "coder_agent" "main" {
arch = "amd64"
auth = "azure-instance-identity"
os = "windows"
}
resource "random_password" "admin_password" {
length = 16
special = true
# https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference
# we remove characters that require special handling in XML, as this is how we pass it to the VM; we also remove the powershell escape character
# namely: <>&'`"
override_special = "~!@#$%^*_-+=|\\(){}[]:;,.?/"
}
locals {
prefix = "coder-win"
admin_username = "coder"
}
resource "azurerm_resource_group" "main" {
name = "${local.prefix}-${data.coder_workspace.me.id}"
location = module.azure_region.value
tags = {
Coder_Provisioned = "true"
}
}
// Uncomment here and in the azurerm_network_interface resource to obtain a public IP
#resource "azurerm_public_ip" "main" {
# name = "publicip"
# resource_group_name = azurerm_resource_group.main.name
# location = azurerm_resource_group.main.location
# allocation_method = "Static"
# tags = {
# Coder_Provisioned = "true"
# }
#}
resource "azurerm_virtual_network" "main" {
name = "network"
address_space = ["10.0.0.0/24"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = {
Coder_Provisioned = "true"
}
}
resource "azurerm_subnet" "internal" {
name = "internal"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.0.0/29"]
}
resource "azurerm_network_interface" "main" {
name = "nic"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.internal.id
private_ip_address_allocation = "Dynamic"
// Uncomment for public IP address as well as azurerm_public_ip resource above
# public_ip_address_id = azurerm_public_ip.main.id
}
tags = {
Coder_Provisioned = "true"
}
}
# Create storage account for boot diagnostics
resource "azurerm_storage_account" "my_storage_account" {
name = "diag${random_id.storage_id.hex}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
account_tier = "Standard"
account_replication_type = "LRS"
}
# Generate random text for a unique storage account name
resource "random_id" "storage_id" {
keepers = {
# Generate a new ID only when a new resource group is defined
resource_group = azurerm_resource_group.main.name
}
byte_length = 8
}
resource "azurerm_managed_disk" "data" {
name = "data_disk"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
storage_account_type = "Standard_LRS"
create_option = "Empty"
disk_size_gb = data.coder_parameter.data_disk_size.value
}
# Create virtual machine
resource "azurerm_windows_virtual_machine" "main" {
count = data.coder_workspace.me.start_count
name = "vm"
admin_username = local.admin_username
admin_password = random_password.admin_password.result
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
network_interface_ids = [azurerm_network_interface.main.id]
size = "Standard_DS1_v2"
custom_data = base64encode(
templatefile("${path.module}/Initialize.ps1.tftpl", { init_script = coder_agent.main.init_script })
)
os_disk {
name = "myOsDisk"
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-datacenter-azure-edition"
version = "latest"
}
additional_unattend_content {
content = "<AutoLogon><Password><Value>${random_password.admin_password.result}</Value></Password><Enabled>true</Enabled><LogonCount>1</LogonCount><Username>${local.admin_username}</Username></AutoLogon>"
setting = "AutoLogon"
}
additional_unattend_content {
content = file("${path.module}/FirstLogonCommands.xml")
setting = "FirstLogonCommands"
}
boot_diagnostics {
storage_account_uri = azurerm_storage_account.my_storage_account.primary_blob_endpoint
}
tags = {
Coder_Provisioned = "true"
}
}
resource "coder_metadata" "rdp_login" {
count = data.coder_workspace.me.start_count
resource_id = azurerm_windows_virtual_machine.main[0].id
item {
key = "Username"
value = local.admin_username
}
item {
key = "Password"
value = random_password.admin_password.result
sensitive = true
}
}
resource "azurerm_virtual_machine_data_disk_attachment" "main_data" {
count = data.coder_workspace.me.start_count
managed_disk_id = azurerm_managed_disk.data.id
virtual_machine_id = azurerm_windows_virtual_machine.main[0].id
lun = "10"
caching = "ReadWrite"
}
@@ -0,0 +1,52 @@
---
display_name: DigitalOcean Droplet (Linux)
description: Provision DigitalOcean Droplets as Coder workspaces
icon: ../../../../.icons/digital-ocean.svg
maintainer_github: coder
verified: true
tags: [vm, linux, digitalocean]
---
# Remote Development on DigitalOcean Droplets
Provision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
To deploy workspaces as DigitalOcean Droplets, you'll need:
- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token)
- DigitalOcean project ID (you can get your project information via the `doctl` CLI by running `doctl projects list`)
- Remove the following sections from the `main.tf` file if you don't want to
associate your workspaces with a project:
- `variable "project_uuid"`
- `resource "digitalocean_project_resources" "project"`
- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running
`doctl compute ssh-key list`)
- Note that this is only required for Fedora images to work.
### Authentication
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Digital Ocean.
Obtain a [Digital Ocean Personal Access Token](https://cloud.digitalocean.com/account/api/tokens) and set the `DIGITALOCEAN_TOKEN` environment variable to the access token.
For other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs).
## Architecture
This template provisions the following resources:
- DigitalOcean VM (ephemeral, deleted on stop)
- Managed disk (persistent, mounted to `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
> [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,46 @@
#cloud-config
users:
- name: ${username}
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
groups: sudo
shell: /bin/bash
packages:
- git
mounts:
- [
"LABEL=${home_volume_label}",
"/home/${username}",
auto,
"defaults,uid=1000,gid=1000",
]
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${init_script}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${username}
ExecStart=/opt/coder/init
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-900
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
- chown ${username}:${username} /home/${username}
- systemctl enable coder-agent
- systemctl start coder-agent
@@ -0,0 +1,361 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
digitalocean = {
source = "digitalocean/digitalocean"
}
}
}
provider "coder" {}
variable "project_uuid" {
type = string
description = <<-EOF
DigitalOcean project ID
$ doctl projects list
EOF
sensitive = true
validation {
# make sure length of alphanumeric string is 36 (UUIDv4 size)
condition = length(var.project_uuid) == 36
error_message = "Invalid Digital Ocean Project ID."
}
}
variable "ssh_key_id" {
type = number
description = <<-EOF
DigitalOcean SSH key ID (some Droplet images require an SSH key to be set):
Can be set to "0" for no key.
Note: Setting this to zero will break Fedora images and notify root passwords via email.
$ doctl compute ssh-key list
EOF
sensitive = true
default = 0
validation {
condition = var.ssh_key_id >= 0
error_message = "Invalid Digital Ocean SSH key ID, a number is required."
}
}
data "coder_parameter" "droplet_image" {
name = "droplet_image"
display_name = "Droplet image"
description = "Which Droplet image would you like to use?"
default = "ubuntu-22-04-x64"
type = "string"
mutable = false
option {
name = "AlmaLinux 9"
value = "almalinux-9-x64"
icon = "/icon/almalinux.svg"
}
option {
name = "AlmaLinux 8"
value = "almalinux-8-x64"
icon = "/icon/almalinux.svg"
}
option {
name = "Fedora 39"
value = "fedora-39-x64"
icon = "/icon/fedora.svg"
}
option {
name = "Fedora 38"
value = "fedora-38-x64"
icon = "/icon/fedora.svg"
}
option {
name = "CentOS Stream 9"
value = "centos-stream-9-x64"
icon = "/icon/centos.svg"
}
option {
name = "CentOS Stream 8"
value = "centos-stream-8-x64"
icon = "/icon/centos.svg"
}
option {
name = "Debian 12"
value = "debian-12-x64"
icon = "/icon/debian.svg"
}
option {
name = "Debian 11"
value = "debian-11-x64"
icon = "/icon/debian.svg"
}
option {
name = "Debian 10"
value = "debian-10-x64"
icon = "/icon/debian.svg"
}
option {
name = "Rocky Linux 9"
value = "rockylinux-9-x64"
icon = "/icon/rockylinux.svg"
}
option {
name = "Rocky Linux 8"
value = "rockylinux-8-x64"
icon = "/icon/rockylinux.svg"
}
option {
name = "Ubuntu 22.04 (LTS)"
value = "ubuntu-22-04-x64"
icon = "/icon/ubuntu.svg"
}
option {
name = "Ubuntu 20.04 (LTS)"
value = "ubuntu-20-04-x64"
icon = "/icon/ubuntu.svg"
}
}
data "coder_parameter" "droplet_size" {
name = "droplet_size"
display_name = "Droplet size"
description = "Which Droplet configuration would you like to use?"
default = "s-1vcpu-1gb"
type = "string"
icon = "/icon/memory.svg"
mutable = false
# s-1vcpu-512mb-10gb is unsupported in tor1, blr1, lon1, sfo2, and nyc3 regions
# s-8vcpu-16gb access requires a support ticket with Digital Ocean
option {
name = "1 vCPU, 1 GB RAM"
value = "s-1vcpu-1gb"
}
option {
name = "1 vCPU, 2 GB RAM"
value = "s-1vcpu-2gb"
}
option {
name = "2 vCPU, 2 GB RAM"
value = "s-2vcpu-2gb"
}
option {
name = "2 vCPU, 4 GB RAM"
value = "s-2vcpu-4gb"
}
option {
name = "4 vCPU, 8 GB RAM"
value = "s-4vcpu-8gb"
}
}
data "coder_parameter" "home_volume_size" {
name = "home_volume_size"
display_name = "Home volume size"
description = "How large would you like your home volume to be (in GB)?"
type = "number"
default = "20"
mutable = false
validation {
min = 1
max = 100 # Sizes larger than 100 GB require a support ticket with Digital Ocean
}
}
data "coder_parameter" "region" {
name = "region"
display_name = "Region"
description = "This is the region where your workspace will be created."
icon = "/emojis/1f30e.png"
type = "string"
default = "ams3"
mutable = false
# nyc1, sfo1, and ams2 regions were excluded because they do not support volumes, which are used to persist data while decreasing cost
option {
name = "Canada (Toronto)"
value = "tor1"
icon = "/emojis/1f1e8-1f1e6.png"
}
option {
name = "Germany (Frankfurt)"
value = "fra1"
icon = "/emojis/1f1e9-1f1ea.png"
}
option {
name = "India (Bangalore)"
value = "blr1"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Netherlands (Amsterdam)"
value = "ams3"
icon = "/emojis/1f1f3-1f1f1.png"
}
option {
name = "Singapore"
value = "sgp1"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "United Kingdom (London)"
value = "lon1"
icon = "/emojis/1f1ec-1f1e7.png"
}
option {
name = "United States (California - 2)"
value = "sfo2"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "United States (California - 3)"
value = "sfo3"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "United States (New York - 1)"
value = "nyc1"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "United States (New York - 3)"
value = "nyc3"
icon = "/emojis/1f1fa-1f1f8.png"
}
}
# Configure the DigitalOcean Provider
provider "digitalocean" {
# Recommended: use environment variable DIGITALOCEAN_TOKEN with your personal access token when starting coderd
# alternatively, you can pass the token via a variable.
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
metadata {
key = "home"
display_name = "Home Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}"
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "digitalocean_volume" "home_volume" {
region = data.coder_parameter.region.value
name = "coder-${data.coder_workspace.me.id}-home"
size = data.coder_parameter.home_volume_size.value
initial_filesystem_type = "ext4"
initial_filesystem_label = "coder-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
}
resource "digitalocean_droplet" "workspace" {
region = data.coder_parameter.region.value
count = data.coder_workspace.me.start_count
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
image = data.coder_parameter.droplet_image.value
size = data.coder_parameter.droplet_size.value
volume_ids = [digitalocean_volume.home_volume.id]
user_data = templatefile("cloud-config.yaml.tftpl", {
username = lower(data.coder_workspace_owner.me.name)
home_volume_label = digitalocean_volume.home_volume.initial_filesystem_label
init_script = base64encode(coder_agent.main.init_script)
coder_agent_token = coder_agent.main.token
})
# Required to provision Fedora.
ssh_keys = var.ssh_key_id > 0 ? [var.ssh_key_id] : []
}
resource "digitalocean_project_resources" "project" {
project = var.project_uuid
# Workaround for terraform plan when using count.
resources = length(digitalocean_droplet.workspace) > 0 ? [
digitalocean_volume.home_volume.urn,
digitalocean_droplet.workspace[0].urn
] : [
digitalocean_volume.home_volume.urn
]
}
resource "coder_metadata" "workspace-info" {
count = data.coder_workspace.me.start_count
resource_id = digitalocean_droplet.workspace[0].id
item {
key = "region"
value = digitalocean_droplet.workspace[0].region
}
item {
key = "image"
value = digitalocean_droplet.workspace[0].image
}
}
resource "coder_metadata" "volume-info" {
resource_id = digitalocean_volume.home_volume.id
item {
key = "size"
value = "${digitalocean_volume.home_volume.size} GiB"
}
}
@@ -0,0 +1,77 @@
---
display_name: Docker (Devcontainer)
description: Provision envbuilder containers as Coder workspaces
icon: ../../../../.icons/docker.svg
maintainer_github: coder
verified: true
tags: [container, docker, devcontainer]
---
# Remote Development on Docker Containers (with Devcontainers)
Provision Devcontainers as [Coder workspaces](https://coder.com/docs/workspaces) in Docker with this example template.
## Prerequisites
### Infrastructure
Coder must have access to a running Docker socket, and the `coder` user must be a member of the `docker` group:
```shell
# Add coder user to Docker group
sudo usermod -aG docker coder
# Restart Coder server
sudo systemctl restart coder
# Test Docker
sudo -u coder docker ps
```
## Architecture
Coder supports Devcontainers via [envbuilder](https://github.com/coder/envbuilder), an open source project. Read more about this in [Coder's documentation](https://coder.com/docs/templates/dev-containers).
This template provisions the following resources:
- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder)
- Docker image (persistent) using [`envbuilder`](https://github.com/coder/envbuilder)
- Docker container (ephemeral)
- Docker volume (persistent on `/workspaces`)
The Git repository is cloned inside the `/workspaces` volume if not present.
Any local changes to the Devcontainer files inside the volume will be applied when you restart the workspace.
Keep in mind that any tools or files outside of `/workspaces` or not added as part of the Devcontainer specification are not persisted.
Edit the `devcontainer.json` instead!
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## Docker-in-Docker
See the [Envbuilder documentation](https://github.com/coder/envbuilder/blob/main/docs/docker.md) for information on running Docker containers inside a devcontainer built by Envbuilder.
## Caching
To speed up your builds, you can use a container registry as a cache.
When creating the template, set the parameter `cache_repo` to a valid Docker repository.
For example, you can run a local registry:
```shell
docker run --detach \
--volume registry-cache:/var/lib/registry \
--publish 5000:5000 \
--name registry-cache \
--net=host \
registry:2
```
Then, when creating the template, enter `localhost:5000/devcontainer-cache` for the parameter `cache_repo`.
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
> [!NOTE]
> We recommend using a registry cache with authentication enabled.
> To allow Envbuilder to authenticate with the registry cache, specify the variable `cache_repo_docker_config_path`
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.
@@ -0,0 +1,372 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "~> 2.0"
}
docker = {
source = "kreuzwerker/docker"
}
envbuilder = {
source = "coder/envbuilder"
}
}
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "coder" {}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
provider "envbuilder" {}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "repo" {
description = "Select a repository to automatically clone and start working with a devcontainer."
display_name = "Repository (auto)"
mutable = true
name = "repo"
option {
name = "vercel/next.js"
description = "The React Framework"
value = "https://github.com/vercel/next.js"
}
option {
name = "home-assistant/core"
description = "🏡 Open source home automation that puts local control and privacy first."
value = "https://github.com/home-assistant/core"
}
option {
name = "discourse/discourse"
description = "A platform for community discussion. Free, open, simple."
value = "https://github.com/discourse/discourse"
}
option {
name = "denoland/deno"
description = "A modern runtime for JavaScript and TypeScript."
value = "https://github.com/denoland/deno"
}
option {
name = "microsoft/vscode"
icon = "/icon/code.svg"
description = "Code editing. Redefined."
value = "https://github.com/microsoft/vscode"
}
option {
name = "Custom"
icon = "/emojis/1f5c3.png"
description = "Specify a custom repo URL below"
value = "custom"
}
order = 1
}
data "coder_parameter" "custom_repo_url" {
default = ""
description = "Optionally enter a custom repository URL, see [awesome-devcontainers](https://github.com/manekinekko/awesome-devcontainers)."
display_name = "Repository URL (custom)"
name = "custom_repo_url"
mutable = true
order = 2
}
data "coder_parameter" "fallback_image" {
default = "codercom/enterprise-base:ubuntu"
description = "This image runs if the devcontainer fails to build."
display_name = "Fallback Image"
mutable = true
name = "fallback_image"
order = 3
}
data "coder_parameter" "devcontainer_builder" {
description = <<-EOF
Image that will build the devcontainer.
We highly recommend using a specific release as the `:latest` tag will change.
Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder
EOF
display_name = "Devcontainer Builder"
mutable = true
name = "devcontainer_builder"
default = "ghcr.io/coder/envbuilder:latest"
order = 4
}
variable "cache_repo" {
default = ""
description = "(Optional) Use a container registry as a cache to speed up builds."
type = string
}
variable "insecure_cache_repo" {
default = false
description = "Enable this option if your cache registry does not serve HTTPS."
type = bool
}
variable "cache_repo_docker_config_path" {
default = ""
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required."
sensitive = true
type = string
}
locals {
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
git_author_email = data.coder_workspace_owner.me.email
repo_url = data.coder_parameter.repo.value == "custom" ? data.coder_parameter.custom_repo_url.value : data.coder_parameter.repo.value
# The envbuilder provider requires a key-value map of environment variables.
envbuilder_env = {
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
# if the cache repo is enabled.
"ENVBUILDER_GIT_URL" : local.repo_url,
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
"CODER_AGENT_TOKEN" : coder_agent.main.token,
# Use the docker gateway if the access URL is 127.0.0.1
"CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
# Use the docker gateway if the access URL is 127.0.0.1
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
"ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}",
}
# Convert the above map to the format expected by the docker provider.
docker_env = [
for k, v in local.envbuilder_env : "${k}=${v}"
]
}
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
count = var.cache_repo_docker_config_path == "" ? 0 : 1
filename = var.cache_repo_docker_config_path
}
resource "docker_image" "devcontainer_builder_image" {
name = local.devcontainer_builder_image
keep_locally = true
}
resource "docker_volume" "workspaces" {
name = "coder-${data.coder_workspace.me.id}"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
# Check for the presence of a prebuilt image in the cache repo
# that we can use instead.
resource "envbuilder_cached_image" "cached" {
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
builder_image = local.devcontainer_builder_image
git_url = local.repo_url
cache_repo = var.cache_repo
extra_env = local.envbuilder_env
insecure = var.insecure_cache_repo
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the environment specified by the envbuilder provider, if available.
env = var.cache_repo == "" ? local.docker_env : envbuilder_cached_image.cached.0.env
# network_mode = "host" # Uncomment if testing with a registry running on `localhost`.
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/workspaces"
volume_name = docker_volume.workspaces.name
read_only = false
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
labels {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
dir = "/workspaces"
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = local.git_author_name
GIT_AUTHOR_EMAIL = local.git_author_email
GIT_COMMITTER_NAME = local.git_author_name
GIT_COMMITTER_EMAIL = local.git_author_email
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic resources, you can use the `coder stat` command.
# If you need more control, you can write your own script.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $HOME"
interval = 60
timeout = 1
}
metadata {
display_name = "CPU Usage (Host)"
key = "4_cpu_usage_host"
script = "coder stat cpu --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Memory Usage (Host)"
key = "5_mem_usage_host"
script = "coder stat mem --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Load Average (Host)"
key = "6_load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
metadata {
display_name = "Swap Usage (Host)"
key = "7_swap_host"
script = <<EOT
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
EOT
interval = 10
timeout = 1
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/workspaces"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "coder_metadata" "container_info" {
count = data.coder_workspace.me.start_count
resource_id = coder_agent.main.id
item {
key = "workspace image"
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
}
item {
key = "git url"
value = local.repo_url
}
item {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
+48
View File
@@ -0,0 +1,48 @@
---
display_name: Docker Containers
description: Provision Docker containers as Coder workspaces
icon: ../../../../.icons/docker.svg
maintainer_github: coder
verified: true
tags: [docker, container]
---
# Remote Development on Docker Containers
Provision Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
### Infrastructure
The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group:
```sh
# Add coder user to Docker group
sudo adduser coder docker
# Restart Coder server
sudo systemctl restart coder
# Test Docker
sudo -u coder docker ps
```
## Architecture
This template provisions the following resources:
- Docker image (built by Docker socket and kept locally)
- Docker container pod (ephemeral)
- Docker volume (persistent on `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
### Editing the image
Edit the `Dockerfile` and run `coder templates push` to update workspaces.
+220
View File
@@ -0,0 +1,220 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
}
}
locals {
username = data.coder_workspace_owner.me.name
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
# Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Prepare user home with default files on first start.
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic resources, you can use the `coder stat` command.
# If you need more control, you can write your own script.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
metadata {
display_name = "CPU Usage (Host)"
key = "4_cpu_usage_host"
script = "coder stat cpu --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Memory Usage (Host)"
key = "5_mem_usage_host"
script = "coder stat mem --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Load Average (Host)"
key = "6_load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
metadata {
display_name = "Swap Usage (Host)"
key = "7_swap_host"
script = <<EOT
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
EOT
interval = 10
timeout = 1
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
# This field becomes outdated if the workspace is renamed but can
# be useful for debugging or cleaning out dangling volumes.
labels {
label = "coder.workspace_name_at_creation"
value = data.coder_workspace.me.name
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
# Uses lower() to avoid Docker restriction on container names.
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# Hostname makes the shell more user friendly: coder@my-workspace:~$
hostname = data.coder_workspace.me.name
# Use the docker gateway if the access URL is 127.0.0.1
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/home/coder"
volume_name = docker_volume.home_volume.name
read_only = false
}
# Add labels in Docker to keep track of orphan resources.
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.owner_id"
value = data.coder_workspace_owner.me.id
}
labels {
label = "coder.workspace_id"
value = data.coder_workspace.me.id
}
labels {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
@@ -0,0 +1,80 @@
---
display_name: Google Compute Engine (Devcontainer)
description: Provision a Devcontainer on Google Compute Engine instances as Coder workspaces
icon: ../../../../.icons/gcp.svg
maintainer_github: coder
verified: true
tags: [vm, linux, gcp, devcontainer]
---
# Remote Development in a Devcontainer on Google Compute Engine
![Architecture Diagram](./architecture.svg)
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Google Cloud. For example, run `gcloud auth application-default login` to
import credentials on the system and user running coderd. For other ways to
authenticate [consult the Terraform
docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).
Coder requires a Google Cloud Service Account to provision workspaces. To create
a service account:
1. Navigate to the [CGP
console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),
and select your Cloud project (if you have more than one project associated
with your account)
1. Provide a service account name (this name is used to generate the service
account ID)
1. Click **Create and continue**, and choose the following IAM roles to grant to
the service account:
- Compute Admin
- Service Account User
Click **Continue**.
1. Click on the created key, and navigate to the **Keys** tab.
1. Click **Add key** > **Create new key**.
1. Generate a **JSON private key**, which will be what you provide to Coder
during the setup process.
## Architecture
This template provisions the following resources:
- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder)
- GCP VM (persistent) with a running Docker daemon
- GCP Disk (persistent, mounted to root)
- [Envbuilder container](https://github.com/coder/envbuilder) inside the GCP VM
Coder persists the root volume. The full filesystem is preserved when the workspace restarts.
When the GCP VM starts, a startup script runs that ensures a running Docker daemon, and starts
an Envbuilder container using this Docker daemon. The Docker socket is also mounted inside the container to allow running Docker containers inside the workspace.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## Caching
To speed up your builds, you can use a container registry as a cache.
When creating the template, set the parameter `cache_repo` to a valid Docker repository in the form `host.tld/path/to/repo`.
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
> [!NOTE]
> We recommend using a registry cache with authentication enabled.
> To allow Envbuilder to authenticate with the registry cache, specify the variable `cache_repo_docker_config_path`
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.
## code-server
`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. Please check [Coder Registry](https://registry.coder.com) for a list of all modules and templates.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

@@ -0,0 +1,341 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
google = {
source = "hashicorp/google"
}
envbuilder = {
source = "coder/envbuilder"
}
}
}
provider "coder" {}
provider "google" {
zone = module.gcp_region.value
project = var.project_id
}
data "google_compute_default_service_account" "default" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "project_id" {
description = "Which Google Compute Project should your workspace live in?"
}
variable "cache_repo" {
default = ""
description = "(Optional) Use a container registry as a cache to speed up builds. Example: host.tld/path/to/repo."
type = string
}
variable "cache_repo_docker_config_path" {
default = ""
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required. This will depend on your Coder setup. Example: `/home/coder/.docker/config.json`."
sensitive = true
type = string
}
# See https://registry.coder.com/modules/coder/gcp-region
module "gcp_region" {
source = "registry.coder.com/coder/gcp-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
regions = ["us", "europe"]
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance Type"
description = "Select an instance type for your workspace."
type = "string"
mutable = false
order = 2
default = "e2-micro"
option {
name = "e2-micro (2C, 1G)"
value = "e2-micro"
}
option {
name = "e2-small (2C, 2G)"
value = "e2-small"
}
option {
name = "e2-medium (2C, 2G)"
value = "e2-medium"
}
}
data "coder_parameter" "fallback_image" {
default = "codercom/enterprise-base:ubuntu"
description = "This image runs if the devcontainer fails to build."
display_name = "Fallback Image"
mutable = true
name = "fallback_image"
order = 3
}
data "coder_parameter" "devcontainer_builder" {
description = <<-EOF
Image that will build the devcontainer.
Find the latest version of Envbuilder here: https://ghcr.io/coder/envbuilder
Be aware that using the `:latest` tag may expose you to breaking changes.
EOF
display_name = "Devcontainer Builder"
mutable = true
name = "devcontainer_builder"
default = "ghcr.io/coder/envbuilder:latest"
order = 4
}
data "coder_parameter" "repo_url" {
name = "repo_url"
display_name = "Repository URL"
default = "https://github.com/coder/envbuilder-starter-devcontainer"
description = "Repository URL"
mutable = true
}
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
count = var.cache_repo_docker_config_path == "" ? 0 : 1
filename = var.cache_repo_docker_config_path
}
# Be careful when modifying the below locals!
locals {
# Ensure Coder username is a valid Linux username
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32))
# Name the container after the workspace and owner.
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# The devcontainer builder image is the image that will build the devcontainer.
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json.
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "")
# The envbuilder provider requires a key-value map of environment variables. Build this here.
envbuilder_env = {
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
# if the cache repo is enabled.
"ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value,
# The agent token is required for the agent to connect to the Coder platform.
"CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""),
# The agent URL is required for the agent to connect to the Coder platform.
"CODER_AGENT_URL" : data.coder_workspace.me.access_url,
# The agent init script is required for the agent to start up. We base64 encode it here
# to avoid quoting issues.
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh",
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
# The fallback image is the image that will run if the devcontainer fails to build.
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
# The following are used to push the image to the cache repo, if defined.
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
# You can add other required environment variables here.
# See: https://github.com/coder/envbuilder/?tab=readme-ov-file#environment-variables
}
# If we have a cached image, use the cached image's environment variables. Otherwise, just use
# the environment variables we've defined above.
docker_env_input = try(envbuilder_cached_image.cached.0.env_map, local.envbuilder_env)
# Convert the above to the list of arguments for the Docker run command.
# The startup script will write this to a file, which the Docker run command will reference.
docker_env_list_base64 = base64encode(join("\n", [for k, v in local.docker_env_input : "${k}=${v}"]))
# Builder image will either be the builder image parameter, or the cached image, if cache is provided.
builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value)
# The GCP VM needs a startup script to set up the environment and start the container. Defining this here.
# NOTE: make sure to test changes by uncommenting the local_file resource at the bottom of this file
# and running `terraform apply` to see the generated script. You should also run shellcheck on the script
# to ensure it is valid.
startup_script = <<-META
#!/usr/bin/env sh
set -eux
# If user does not exist, create it and set up passwordless sudo
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then
useradd -m -s /bin/bash "${local.linux_user}"
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user
fi
# Check for Docker, install if not present
if ! command -v docker >/dev/null 2>&1; then
echo "Docker not found, installing..."
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh >/dev/null 2>&1
sudo usermod -aG docker ${local.linux_user}
newgrp docker
else
echo "Docker is already installed."
fi
# Write the Docker config JSON to disk if it is provided.
if [ -n "${local.docker_config_json_base64}" ]; then
mkdir -p "/home/${local.linux_user}/.docker"
printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json"
chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker"
fi
# Write the container env to disk.
printf "%s" "${local.docker_env_list_base64}" | base64 -d | tee "/home/${local.linux_user}/env.txt"
# Start envbuilder.
docker run \
--rm \
--net=host \
-h ${lower(data.coder_workspace.me.name)} \
-v /home/${local.linux_user}/envbuilder:/workspaces \
-v /var/run/docker.sock:/var/run/docker.sock \
--env-file /home/${local.linux_user}/env.txt \
${local.builder_image}
META
}
# Create a persistent disk to store the workspace data.
resource "google_compute_disk" "root" {
name = "coder-${data.coder_workspace.me.id}-root"
type = "pd-ssd"
image = "debian-cloud/debian-12"
lifecycle {
ignore_changes = all
}
}
# Check for the presence of a prebuilt image in the cache repo
# that we can use instead.
resource "envbuilder_cached_image" "cached" {
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
builder_image = local.devcontainer_builder_image
git_url = data.coder_parameter.repo_url.value
cache_repo = var.cache_repo
extra_env = local.envbuilder_env
}
# This is useful for debugging the startup script. Left here for reference.
# resource local_file "startup_script" {
# content = local.startup_script
# filename = "${path.module}/startup_script.sh"
# }
# Create a VM where the workspace will run.
resource "google_compute_instance" "vm" {
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root"
machine_type = data.coder_parameter.instance_type.value
# data.coder_workspace_owner.me.name == "default" is a workaround to suppress error in the terraform plan phase while creating a new workspace.
desired_status = (data.coder_workspace_owner.me.name == "default" || data.coder_workspace.me.start_count == 1) ? "RUNNING" : "TERMINATED"
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
boot_disk {
auto_delete = false
source = google_compute_disk.root.name
}
service_account {
email = data.google_compute_default_service_account.default.email
scopes = ["cloud-platform"]
}
metadata = {
# The startup script runs as root with no $HOME environment set up, so instead of directly
# running the agent init script, create a user (with a homedir, default shell and sudo
# permissions) and execute the init script as that user.
startup-script = local.startup_script
}
}
# Create a Coder agent to manage the workspace.
resource "coder_agent" "dev" {
count = data.coder_workspace.me.start_count
arch = "amd64"
auth = "token"
os = "linux"
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
connection_timeout = 0
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
metadata {
key = "disk"
display_name = "Disk Usage"
interval = 5
timeout = 5
script = "coder stat disk"
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/workspaces"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
# Create metadata for the workspace and home disk.
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = google_compute_instance.vm.id
item {
key = "type"
value = google_compute_instance.vm.machine_type
}
item {
key = "zone"
value = module.gcp_region.value
}
}
resource "coder_metadata" "home_info" {
resource_id = google_compute_disk.root.id
item {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
@@ -0,0 +1,64 @@
---
display_name: Google Compute Engine (Linux)
description: Provision Google Compute Engine instances as Coder workspaces
icon: ../../../../.icons/gcp.svg
maintainer_github: coder
verified: true
tags: [vm, linux, gcp]
---
# Remote Development on Google Compute Engine (Linux)
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Google Cloud. For example, run `gcloud auth application-default login` to
import credentials on the system and user running coderd. For other ways to
authenticate [consult the Terraform
docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).
Coder requires a Google Cloud Service Account to provision workspaces. To create
a service account:
1. Navigate to the [CGP
console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),
and select your Cloud project (if you have more than one project associated
with your account)
1. Provide a service account name (this name is used to generate the service
account ID)
1. Click **Create and continue**, and choose the following IAM roles to grant to
the service account:
- Compute Admin
- Service Account User
Click **Continue**.
1. Click on the created key, and navigate to the **Keys** tab.
1. Click **Add key** > **Create new key**.
1. Generate a **JSON private key**, which will be what you provide to Coder
during the setup process.
## Architecture
This template provisions the following resources:
- GCP VM (ephemeral)
- GCP Disk (persistent, mounted to root)
Coder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## code-server
`code-server` is installed via the `startup_script` argument in the `coder_agent`
resource block. The `coder_app` resource is defined to access `code-server` through
the dashboard UI over `localhost:13337`.
+184
View File
@@ -0,0 +1,184 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
google = {
source = "hashicorp/google"
}
}
}
provider "coder" {}
variable "project_id" {
description = "Which Google Compute Project should your workspace live in?"
}
# See https://registry.coder.com/modules/coder/gcp-region
module "gcp_region" {
source = "registry.coder.com/coder/gcp-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
regions = ["us", "europe"]
default = "us-central1-a"
}
provider "google" {
zone = module.gcp_region.value
project = var.project_id
}
data "google_compute_default_service_account" "default" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "google_compute_disk" "root" {
name = "coder-${data.coder_workspace.me.id}-root"
type = "pd-ssd"
zone = module.gcp_region.value
image = "debian-cloud/debian-11"
lifecycle {
ignore_changes = [name, image]
}
}
resource "coder_agent" "main" {
auth = "google-instance-identity"
arch = "amd64"
os = "linux"
startup_script = <<-EOT
set -e
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = <<-EOT
#!/bin/bash
set -e
top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}'
EOT
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = <<-EOT
#!/bin/bash
set -e
free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }'
EOT
}
metadata {
key = "disk"
display_name = "Disk Usage"
interval = 600 # every 10 minutes
timeout = 30 # df can take a while on large filesystems
script = <<-EOT
#!/bin/bash
set -e
df /home/coder | awk '$NF=="/"{printf "%s", $5}'
EOT
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "google_compute_instance" "dev" {
zone = module.gcp_region.value
count = data.coder_workspace.me.start_count
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root"
machine_type = "e2-medium"
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
boot_disk {
auto_delete = false
source = google_compute_disk.root.name
}
service_account {
email = data.google_compute_default_service_account.default.email
scopes = ["cloud-platform"]
}
# The startup script runs as root with no $HOME environment set up, so instead of directly
# running the agent init script, create a user (with a homedir, default shell and sudo
# permissions) and execute the init script as that user.
metadata_startup_script = <<EOMETA
#!/usr/bin/env sh
set -eux
# If user does not exist, create it and set up passwordless sudo
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then
useradd -m -s /bin/bash "${local.linux_user}"
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user
fi
exec sudo -u "${local.linux_user}" sh -c '${coder_agent.main.init_script}'
EOMETA
}
locals {
# Ensure Coder username is a valid Linux username
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32))
}
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = google_compute_instance.dev[0].id
item {
key = "type"
value = google_compute_instance.dev[0].machine_type
}
}
resource "coder_metadata" "home_info" {
resource_id = google_compute_disk.root.id
item {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
@@ -0,0 +1,65 @@
---
display_name: Google Compute Engine (VM Container)
description: Provision Google Compute Engine instances as Coder workspaces
icon: ../../../../.icons/gcp.svg
maintainer_github: coder
verified: true
tags: [vm-container, linux, gcp]
---
# Remote Development on Google Compute Engine (VM Container)
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Google Cloud. For example, run `gcloud auth application-default login` to
import credentials on the system and user running coderd. For other ways to
authenticate [consult the Terraform
docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).
Coder requires a Google Cloud Service Account to provision workspaces. To create
a service account:
1. Navigate to the [CGP
console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),
and select your Cloud project (if you have more than one project associated
with your account)
1. Provide a service account name (this name is used to generate the service
account ID)
1. Click **Create and continue**, and choose the following IAM roles to grant to
the service account:
- Compute Admin
- Service Account User
Click **Continue**.
1. Click on the created key, and navigate to the **Keys** tab.
1. Click **Add key** > **Create new key**.
1. Generate a **JSON private key**, which will be what you provide to Coder
during the setup process.
## Architecture
This template provisions the following resources:
- GCP VM (ephemeral, deleted on stop)
- Container in VM
- Managed disk (persistent, mounted to `/home/coder` in container)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## code-server
`code-server` is installed via the `startup_script` argument in the `coder_agent`
resource block. The `coder_app` resource is defined to access `code-server` through
the dashboard UI over `localhost:13337`.
@@ -0,0 +1,136 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
google = {
source = "hashicorp/google"
}
}
}
provider "coder" {}
variable "project_id" {
description = "Which Google Compute Project should your workspace live in?"
}
# https://registry.coder.com/modules/coder/gcp-region/coder
module "gcp_region" {
source = "registry.coder.com/coder/gcp-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
regions = ["us", "europe"]
}
provider "google" {
zone = module.gcp_region.value
project = var.project_id
}
data "google_compute_default_service_account" "default" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
auth = "google-instance-identity"
arch = "amd64"
os = "linux"
startup_script = <<-EOT
set -e
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
# See https://registry.terraform.io/modules/terraform-google-modules/container-vm
module "gce-container" {
source = "terraform-google-modules/container-vm/google"
version = "3.0.0"
container = {
image = "codercom/enterprise-base:ubuntu"
command = ["sh"]
args = ["-c", coder_agent.main.init_script]
securityContext = {
privileged : true
}
}
}
resource "google_compute_instance" "dev" {
zone = module.gcp_region.value
count = data.coder_workspace.me.start_count
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
machine_type = "e2-medium"
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
boot_disk {
initialize_params {
image = module.gce-container.source_image
}
}
service_account {
email = data.google_compute_default_service_account.default.email
scopes = ["cloud-platform"]
}
metadata = {
"gce-container-declaration" = module.gce-container.metadata_value
}
labels = {
container-vm = module.gce-container.vm_container_label
}
}
resource "coder_agent_instance" "dev" {
count = data.coder_workspace.me.start_count
agent_id = coder_agent.main.id
instance_id = google_compute_instance.dev[0].instance_id
}
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = google_compute_instance.dev[0].id
item {
key = "image"
value = module.gce-container.container.image
}
}
@@ -0,0 +1,64 @@
---
display_name: Google Compute Engine (Windows)
description: Provision Google Compute Engine instances as Coder workspaces
icon: ../../../../.icons/gcp.svg
maintainer_github: coder
verified: true
tags: [vm, windows, gcp]
---
# Remote Development on Google Compute Engine (Windows)
## Prerequisites
### Authentication
This template assumes that coderd is run in an environment that is authenticated
with Google Cloud. For example, run `gcloud auth application-default login` to
import credentials on the system and user running coderd. For other ways to
authenticate [consult the Terraform
docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials).
Coder requires a Google Cloud Service Account to provision workspaces. To create
a service account:
1. Navigate to the [CGP
console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create),
and select your Cloud project (if you have more than one project associated
with your account)
1. Provide a service account name (this name is used to generate the service
account ID)
1. Click **Create and continue**, and choose the following IAM roles to grant to
the service account:
- Compute Admin
- Service Account User
Click **Continue**.
1. Click on the created key, and navigate to the **Keys** tab.
1. Click **Add key** > **Create new key**.
1. Generate a **JSON private key**, which will be what you provide to Coder
during the setup process.
## Architecture
This template provisions the following resources:
- GCP VM (ephemeral)
- GCP Disk (persistent, mounted to root)
Coder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## code-server
`code-server` is installed via the `startup_script` argument in the `coder_agent`
resource block. The `coder_app` resource is defined to access `code-server` through
the dashboard UI over `localhost:13337`.
@@ -0,0 +1,96 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
google = {
source = "hashicorp/google"
}
}
}
provider "coder" {}
variable "project_id" {
description = "Which Google Compute Project should your workspace live in?"
}
# See https://registry.coder.com/modules/coder/gcp-region
module "gcp_region" {
source = "registry.coder.com/coder/gcp-region/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
regions = ["us", "europe"]
default = "us-central1-a"
}
provider "google" {
zone = module.gcp_region.value
project = var.project_id
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "google_compute_default_service_account" "default" {}
resource "google_compute_disk" "root" {
name = "coder-${data.coder_workspace.me.id}-root"
type = "pd-ssd"
zone = module.gcp_region.value
image = "projects/windows-cloud/global/images/windows-server-2022-dc-core-v20220215"
lifecycle {
ignore_changes = [name, image]
}
}
resource "coder_agent" "main" {
auth = "google-instance-identity"
arch = "amd64"
os = "windows"
}
resource "google_compute_instance" "dev" {
zone = module.gcp_region.value
count = data.coder_workspace.me.start_count
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
machine_type = "e2-medium"
network_interface {
network = "default"
access_config {
// Ephemeral public IP
}
}
boot_disk {
auto_delete = false
source = google_compute_disk.root.name
}
service_account {
email = data.google_compute_default_service_account.default.email
scopes = ["cloud-platform"]
}
metadata = {
windows-startup-script-ps1 = coder_agent.main.init_script
serial-port-enable = "TRUE"
}
}
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = google_compute_instance.dev[0].id
item {
key = "type"
value = google_compute_instance.dev[0].machine_type
}
}
resource "coder_metadata" "home_info" {
resource_id = google_compute_disk.root.id
item {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
+51
View File
@@ -0,0 +1,51 @@
---
display_name: Incus System Container with Docker
description: Develop in an Incus System Container with Docker using incus
icon: ../../../../.icons/lxc.svg
maintainer_github: coder
verified: true
tags: [local, incus, lxc, lxd]
---
# Incus System Container with Docker
Develop in an Incus System Container and run nested Docker containers using Incus on your local infrastructure.
## Prerequisites
1. Install [Incus](https://linuxcontainers.org/incus/) on the same machine as Coder.
2. Allow Coder to access the Incus socket.
- If you're running Coder as system service, run `sudo usermod -aG incus-admin coder` and restart the Coder service.
- If you're running Coder as a Docker Compose service, get the group ID of the `incus-admin` group by running `getent group incus-admin` and add the following to your `compose.yaml` file:
```yaml
services:
coder:
volumes:
- /var/lib/incus/unix.socket:/var/lib/incus/unix.socket
group_add:
- 996 # Replace with the group ID of the `incus-admin` group
```
3. Create a storage pool named `coder` and `btrfs` as the driver by running `incus storage create coder btrfs`.
## Usage
> **Note:** this template requires using a container image with cloud-init installed such as `ubuntu/jammy/cloud/amd64`.
1. Run `coder templates init -id incus`
1. Select this template
1. Follow the on-screen instructions
## Extending this template
See the [lxc/incus](https://registry.terraform.io/providers/lxc/incus/latest/docs) Terraform provider documentation to
add the following features to your Coder template:
- HTTPS incus host
- Volume mounts
- Custom networks
- More
We also welcome contributions!
+317
View File
@@ -0,0 +1,317 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
incus = {
source = "lxc/incus"
}
}
}
data "coder_provisioner" "me" {}
provider "incus" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "image" {
name = "image"
display_name = "Image"
description = "The container image to use. Be sure to use a variant with cloud-init installed!"
default = "ubuntu/jammy/cloud/amd64"
icon = "/icon/image.svg"
mutable = true
}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "The number of CPUs to allocate to the workspace (1-8)"
type = "number"
default = "1"
icon = "https://raw.githubusercontent.com/matifali/logos/main/cpu-3.svg"
mutable = true
validation {
min = 1
max = 8
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory"
description = "The amount of memory to allocate to the workspace in GB (up to 16GB)"
type = "number"
default = "2"
icon = "/icon/memory.svg"
mutable = true
validation {
min = 1
max = 16
}
}
data "coder_parameter" "git_repo" {
type = "string"
name = "Git repository"
default = "https://github.com/coder/coder"
description = "Clone a git repo into [base directory]"
mutable = true
}
data "coder_parameter" "repo_base_dir" {
type = "string"
name = "Repository Base Directory"
default = "~"
description = "The directory specified will be created (if missing) and the specified repo will be cloned into [base directory]/{repo}🪄."
mutable = true
}
resource "coder_agent" "main" {
count = data.coder_workspace.me.start_count
arch = data.coder_provisioner.me.arch
os = "linux"
dir = "/home/${local.workspace_user}"
env = {
CODER_WORKSPACE_ID = data.coder_workspace.me.id
}
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}"
interval = 60
timeout = 1
}
}
# https://registry.coder.com/modules/coder/git-clone
module "git-clone" {
source = "registry.coder.com/coder/git-clone/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = local.agent_id
url = data.coder_parameter.git_repo.value
base_dir = local.repo_base_dir
}
# https://registry.coder.com/modules/coder/code-server
module "code-server" {
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = local.agent_id
folder = local.repo_base_dir
}
# https://registry.coder.com/modules/coder/filebrowser
module "filebrowser" {
source = "registry.coder.com/coder/filebrowser/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = local.agent_id
}
# https://registry.coder.com/modules/coder/coder-login
module "coder-login" {
source = "registry.coder.com/coder/coder-login/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = local.agent_id
}
resource "incus_volume" "home" {
name = "coder-${data.coder_workspace.me.id}-home"
pool = local.pool
}
resource "incus_volume" "docker" {
name = "coder-${data.coder_workspace.me.id}-docker"
pool = local.pool
}
resource "incus_cached_image" "image" {
source_remote = "images"
source_image = data.coder_parameter.image.value
}
resource "incus_instance_file" "agent_token" {
count = data.coder_workspace.me.start_count
instance = incus_instance.dev.name
content = <<EOF
CODER_AGENT_TOKEN=${local.agent_token}
EOF
create_directories = true
target_path = "/opt/coder/init.env"
}
resource "incus_instance" "dev" {
running = data.coder_workspace.me.start_count == 1
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
image = incus_cached_image.image.fingerprint
config = {
"security.nesting" = true
"security.syscalls.intercept.mknod" = true
"security.syscalls.intercept.setxattr" = true
"boot.autostart" = true
"cloud-init.user-data" = <<EOF
#cloud-config
hostname: ${lower(data.coder_workspace.me.name)}
users:
- name: ${local.workspace_user}
uid: 1000
gid: 1000
groups: sudo
packages:
- curl
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
write_files:
- path: /opt/coder/init
permissions: "0755"
encoding: b64
content: ${base64encode(local.agent_init_script)}
- path: /etc/systemd/system/coder-agent.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent
After=network-online.target
Wants=network-online.target
[Service]
User=${local.workspace_user}
EnvironmentFile=/opt/coder/init.env
ExecStart=/opt/coder/init
Restart=always
RestartSec=10
TimeoutStopSec=90
KillMode=process
OOMScoreAdjust=-900
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
- path: /etc/systemd/system/coder-agent-watcher.service
permissions: "0644"
content: |
[Unit]
Description=Coder Agent Watcher
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl restart coder-agent.service
[Install]
WantedBy=multi-user.target
- path: /etc/systemd/system/coder-agent-watcher.path
permissions: "0644"
content: |
[Path]
PathModified=/opt/coder/init.env
Unit=coder-agent-watcher.service
[Install]
WantedBy=multi-user.target
runcmd:
- chown -R ${local.workspace_user}:${local.workspace_user} /home/${local.workspace_user}
- |
#!/bin/bash
apt-get update && apt-get install -y curl docker.io
usermod -aG docker ${local.workspace_user}
newgrp docker
- systemctl enable coder-agent.service coder-agent-watcher.service coder-agent-watcher.path
- systemctl start coder-agent.service coder-agent-watcher.service coder-agent-watcher.path
EOF
}
limits = {
cpu = data.coder_parameter.cpu.value
memory = "${data.coder_parameter.cpu.value}GiB"
}
device {
name = "home"
type = "disk"
properties = {
path = "/home/${local.workspace_user}"
pool = local.pool
source = incus_volume.home.name
}
}
device {
name = "docker"
type = "disk"
properties = {
path = "/var/lib/docker"
pool = local.pool
source = incus_volume.docker.name
}
}
device {
name = "root"
type = "disk"
properties = {
path = "/"
pool = local.pool
}
}
}
locals {
workspace_user = lower(data.coder_workspace_owner.me.name)
pool = "coder"
repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/${local.workspace_user}" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/${local.workspace_user}/")
repo_dir = module.git-clone.repo_dir
agent_id = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].id : ""
agent_token = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].token : ""
agent_init_script = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].init_script : ""
}
resource "coder_metadata" "info" {
count = data.coder_workspace.me.start_count
resource_id = incus_instance.dev.name
item {
key = "memory"
value = incus_instance.dev.limits.memory
}
item {
key = "cpus"
value = incus_instance.dev.limits.cpu
}
item {
key = "instance"
value = incus_instance.dev.name
}
item {
key = "image"
value = "${incus_cached_image.image.source_remote}:${incus_cached_image.image.source_image}"
}
item {
key = "image_fingerprint"
value = substr(incus_cached_image.image.fingerprint, 0, 12)
}
}
@@ -0,0 +1,58 @@
---
display_name: Kubernetes (Devcontainer)
description: Provision envbuilder pods as Coder workspaces
icon: ../../../../.icons/kubernetes.svg
maintainer_github: coder
verified: true
tags: [container, kubernetes, devcontainer]
---
# Remote Development on Kubernetes Pods (with Devcontainers)
Provision Devcontainers as [Coder workspaces](https://coder.com/docs/workspaces) on Kubernetes with this example template.
## Prerequisites
### Infrastructure
**Cluster**: This template requires an existing Kubernetes cluster.
**Container Image**: This template uses the [envbuilder image](https://github.com/coder/envbuilder) to build a Devcontainer from a `devcontainer.json`.
**(Optional) Cache Registry**: Envbuilder can utilize a Docker registry as a cache to speed up workspace builds. The [envbuilder Terraform provider](https://github.com/coder/terraform-provider-envbuilder) will check the contents of the cache to determine if a prebuilt image exists. In the case of some missing layers in the registry (partial cache miss), Envbuilder can still utilize some of the build cache from the registry.
### Authentication
This template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template.
## Architecture
Coder supports devcontainers with [envbuilder](https://github.com/coder/envbuilder), an open source project. Read more about this in [Coder's documentation](https://coder.com/docs/templates/dev-containers).
This template provisions the following resources:
- Kubernetes deployment (ephemeral)
- Kubernetes persistent volume claim (persistent on `/workspaces`)
- Envbuilder cached image (optional, persistent).
This template will fetch a Git repo containing a `devcontainer.json` specified by the `repo` parameter, and builds it
with [`envbuilder`](https://github.com/coder/envbuilder).
The Git repository is cloned inside the `/workspaces` volume if not present.
Any local changes to the Devcontainer files inside the volume will be applied when you restart the workspace.
As you might suspect, any tools or files outside of `/workspaces` or not added as part of the Devcontainer specification are not persisted.
Edit the `devcontainer.json` instead!
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## Caching
To speed up your builds, you can use a container registry as a cache.
When creating the template, set the parameter `cache_repo`.
See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.
> [!NOTE]
> We recommend using a registry cache with authentication enabled.
> To allow Envbuilder to authenticate with the registry cache, specify the variable `cache_repo_dockerconfig_secret`
> with the name of a Kubernetes secret in the same namespace as Coder. The secret must contain the key `.dockerconfigjson`.
@@ -0,0 +1,464 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "~> 2.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
envbuilder = {
source = "coder/envbuilder"
}
}
}
provider "coder" {}
provider "kubernetes" {
# Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences
config_path = var.use_kubeconfig == true ? "~/.kube/config" : null
}
provider "envbuilder" {}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "use_kubeconfig" {
type = bool
description = <<-EOF
Use host kubeconfig? (true/false)
Set this to false if the Coder host is itself running as a Pod on the same
Kubernetes cluster as you are deploying workspaces to.
Set this to true if the Coder host is running outside the Kubernetes cluster
for workspaces. A valid "~/.kube/config" must be present on the Coder host.
EOF
default = false
}
variable "namespace" {
type = string
default = "default"
description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace."
}
variable "cache_repo" {
default = ""
description = "Use a container registry as a cache to speed up builds."
type = string
}
variable "insecure_cache_repo" {
default = false
description = "Enable this option if your cache registry does not serve HTTPS."
type = bool
}
data "coder_parameter" "cpu" {
type = "number"
name = "cpu"
display_name = "CPU"
description = "CPU limit (cores)."
default = "2"
icon = "/emojis/1f5a5.png"
mutable = true
validation {
min = 1
max = 99999
}
order = 1
}
data "coder_parameter" "memory" {
type = "number"
name = "memory"
display_name = "Memory"
description = "Memory limit (GiB)."
default = "2"
icon = "/icon/memory.svg"
mutable = true
validation {
min = 1
max = 99999
}
order = 2
}
data "coder_parameter" "workspaces_volume_size" {
name = "workspaces_volume_size"
display_name = "Workspaces volume size"
description = "Size of the `/workspaces` volume (GiB)."
default = "10"
type = "number"
icon = "/emojis/1f4be.png"
mutable = false
validation {
min = 1
max = 99999
}
order = 3
}
data "coder_parameter" "repo" {
description = "Select a repository to automatically clone and start working with a devcontainer."
display_name = "Repository (auto)"
mutable = true
name = "repo"
order = 4
type = "string"
}
data "coder_parameter" "fallback_image" {
default = "codercom/enterprise-base:ubuntu"
description = "This image runs if the devcontainer fails to build."
display_name = "Fallback Image"
mutable = true
name = "fallback_image"
order = 6
}
data "coder_parameter" "devcontainer_builder" {
description = <<-EOF
Image that will build the devcontainer.
We highly recommend using a specific release as the `:latest` tag will change.
Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder
EOF
display_name = "Devcontainer Builder"
mutable = true
name = "devcontainer_builder"
default = "ghcr.io/coder/envbuilder:latest"
order = 7
}
variable "cache_repo_secret_name" {
default = ""
description = "Path to a docker config.json containing credentials to the provided cache repo, if required."
sensitive = true
type = string
}
data "kubernetes_secret" "cache_repo_dockerconfig_secret" {
count = var.cache_repo_secret_name == "" ? 0 : 1
metadata {
name = var.cache_repo_secret_name
namespace = var.namespace
}
}
locals {
deployment_name = "coder-${lower(data.coder_workspace.me.id)}"
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
git_author_email = data.coder_workspace_owner.me.email
repo_url = data.coder_parameter.repo.value
# The envbuilder provider requires a key-value map of environment variables.
envbuilder_env = {
"CODER_AGENT_TOKEN" : coder_agent.main.token,
# Use the docker gateway if the access URL is 127.0.0.1
"CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
# if the cache repo is enabled.
"ENVBUILDER_GIT_URL" : var.cache_repo == "" ? local.repo_url : "",
# Use the docker gateway if the access URL is 127.0.0.1
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
"ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")),
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true"
# You may need to adjust this if you get an error regarding deleting files when building the workspace.
# For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in
# addition to `/var/run`.
# "ENVBUILDER_IGNORE_PATHS": "/product_name,/product_uuid,/var/run",
}
}
# Check for the presence of a prebuilt image in the cache repo
# that we can use instead.
resource "envbuilder_cached_image" "cached" {
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
builder_image = local.devcontainer_builder_image
git_url = local.repo_url
cache_repo = var.cache_repo
extra_env = local.envbuilder_env
insecure = var.insecure_cache_repo
}
resource "kubernetes_persistent_volume_claim" "workspaces" {
metadata {
name = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
"app.kubernetes.io/instance" = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
"app.kubernetes.io/part-of" = "coder"
//Coder-specific labels.
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
wait_until_bound = false
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${data.coder_parameter.workspaces_volume_size.value}Gi"
}
}
# storage_class_name = "local-path" # Configure the StorageClass to use here, if required.
}
}
resource "kubernetes_deployment" "main" {
count = data.coder_workspace.me.start_count
depends_on = [
kubernetes_persistent_volume_claim.workspaces
]
wait_for_rollout = false
metadata {
name = local.deployment_name
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = local.deployment_name
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
spec {
replicas = 1
selector {
match_labels = {
"app.kubernetes.io/name" = "coder-workspace"
}
}
strategy {
type = "Recreate"
}
template {
metadata {
labels = {
"app.kubernetes.io/name" = "coder-workspace"
}
}
spec {
security_context {}
container {
name = "dev"
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
image_pull_policy = "Always"
security_context {}
# Set the environment using cached_image.cached.0.env if the cache repo is enabled.
# Otherwise, use the local.envbuilder_env.
# You could alternatively write the environment variables to a ConfigMap or Secret
# and use that as `env_from`.
dynamic "env" {
for_each = nonsensitive(var.cache_repo == "" ? local.envbuilder_env : envbuilder_cached_image.cached.0.env_map)
content {
name = env.key
value = env.value
}
}
resources {
requests = {
"cpu" = "250m"
"memory" = "512Mi"
}
limits = {
"cpu" = "${data.coder_parameter.cpu.value}"
"memory" = "${data.coder_parameter.memory.value}Gi"
}
}
volume_mount {
mount_path = "/workspaces"
name = "workspaces"
read_only = false
}
}
volume {
name = "workspaces"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name
read_only = false
}
}
affinity {
// This affinity attempts to spread out all workspace pods evenly across
// nodes.
pod_anti_affinity {
preferred_during_scheduling_ignored_during_execution {
weight = 1
pod_affinity_term {
topology_key = "kubernetes.io/hostname"
label_selector {
match_expressions {
key = "app.kubernetes.io/name"
operator = "In"
values = ["coder-workspace"]
}
}
}
}
}
}
}
}
}
}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
dir = "/workspaces"
# These environment variables allow you to make Git commits right away after creating a
# workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
# You can remove this block if you'd prefer to configure Git manually or using
# dotfiles. (see docs/dotfiles.md)
env = {
GIT_AUTHOR_NAME = local.git_author_name
GIT_AUTHOR_EMAIL = local.git_author_email
GIT_COMMITTER_NAME = local.git_author_name
GIT_COMMITTER_EMAIL = local.git_author_email
}
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic resources, you can use the `coder stat` command.
# If you need more control, you can write your own script.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Workspaces Disk"
key = "3_workspaces_disk"
script = "coder stat disk --path /workspaces"
interval = 60
timeout = 1
}
metadata {
display_name = "CPU Usage (Host)"
key = "4_cpu_usage_host"
script = "coder stat cpu --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Memory Usage (Host)"
key = "5_mem_usage_host"
script = "coder stat mem --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Load Average (Host)"
key = "6_load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
metadata {
display_name = "Swap Usage (Host)"
key = "7_swap_host"
script = <<EOT
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
EOT
interval = 10
timeout = 1
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "coder_metadata" "container_info" {
count = data.coder_workspace.me.start_count
resource_id = coder_agent.main.id
item {
key = "workspace image"
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
}
item {
key = "git url"
value = local.repo_url
}
item {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
@@ -0,0 +1,59 @@
---
display_name: Kubernetes (Envbox)
description: Provision envbox pods as Coder workspaces
icon: ../../../../.icons/kubernetes.svg
maintainer_github: coder
verified: true
tags: [kubernetes, containers, docker-in-docker]
---
# envbox
## Introduction
`envbox` is an image that enables creating non-privileged containers capable of running system-level software (e.g. `dockerd`, `systemd`, etc) in Kubernetes.
It mainly acts as a wrapper for the excellent [sysbox runtime](https://github.com/nestybox/sysbox/) developed by [Nestybox](https://www.nestybox.com/). For more details on the security of `sysbox` containers see sysbox's [official documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/security.md).
## Envbox Configuration
The following environment variables can be used to configure various aspects of the inner and outer container.
| env | usage | required |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `CODER_INNER_IMAGE` | The image to use for the inner container. | True |
| `CODER_INNER_USERNAME` | The username to use for the inner container. | True |
| `CODER_AGENT_TOKEN` | The [Coder Agent](https://coder.com/docs/about/architecture#agents) token to pass to the inner container. | True |
| `CODER_INNER_ENVS` | The environment variables to pass to the inner container. A wildcard can be used to match a prefix. Ex: `CODER_INNER_ENVS=KUBERNETES_*,MY_ENV,MY_OTHER_ENV` | false |
| `CODER_INNER_HOSTNAME` | The hostname to use for the inner container. | false |
| `CODER_IMAGE_PULL_SECRET` | The docker credentials to use when pulling the inner container. The recommended way to do this is to create an [Image Pull Secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials) and then reference the secret using an [environment variable](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data). | false |
| `CODER_DOCKER_BRIDGE_CIDR` | The bridge CIDR to start the Docker daemon with. | false |
| `CODER_MOUNTS` | A list of mounts to mount into the inner container. Mounts default to `rw`. Ex: `CODER_MOUNTS=/home/coder:/home/coder,/var/run/mysecret:/var/run/mysecret:ro` | false |
| `CODER_USR_LIB_DIR` | The mountpoint of the host `/usr/lib` directory. Only required when using GPUs. | false |
| `CODER_ADD_TUN` | If `CODER_ADD_TUN=true` add a TUN device to the inner container. | false |
| `CODER_ADD_FUSE` | If `CODER_ADD_FUSE=true` add a FUSE device to the inner container. | false |
| `CODER_ADD_GPU` | If `CODER_ADD_GPU=true` add detected GPUs and related files to the inner container. Requires setting `CODER_USR_LIB_DIR` and mounting in the hosts `/usr/lib/` directory. | false |
| `CODER_CPUS` | Dictates the number of CPUs to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
| `CODER_MEMORY` | Dictates the max memory (in bytes) to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
## Migrating Existing Envbox Templates
Due to the [deprecation and removal of legacy parameters](https://coder.com/docs/templates/parameters#legacy)
it may be necessary to migrate existing envbox templates on newer versions of
Coder. Consult the [migration](https://coder.com/docs/templates/parameters#migration)
documentation for details on how to do so.
To supply values to existing existing Terraform variables you can specify the
`-V` flag. For example
```bash
coder templates push envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1
```
## Version Pinning
The template sets the image tag as `latest`. We highly recommend pinning the image to a specific release of envbox, as the `latest` tag may change.
## Contributions
Contributions are welcome and can be made against the [envbox repo](https://github.com/coder/envbox).
@@ -0,0 +1,322 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
data "coder_parameter" "home_disk" {
name = "Disk Size"
description = "How large should the disk storing the home directory be?"
icon = "https://cdn-icons-png.flaticon.com/512/2344/2344147.png"
type = "number"
default = 10
mutable = true
validation {
min = 10
max = 100
}
}
variable "use_kubeconfig" {
type = bool
default = true
description = <<-EOF
Use host kubeconfig? (true/false)
Set this to false if the Coder host is itself running as a Pod on the same
Kubernetes cluster as you are deploying workspaces to.
Set this to true if the Coder host is running outside the Kubernetes cluster
for workspaces. A valid "~/.kube/config" must be present on the Coder host.
EOF
}
provider "coder" {}
variable "namespace" {
type = string
description = "The namespace to create workspaces in (must exist prior to creating workspaces)"
}
variable "create_tun" {
type = bool
description = "Add a TUN device to the workspace."
default = false
}
variable "create_fuse" {
type = bool
description = "Add a FUSE device to the workspace."
default = false
}
variable "max_cpus" {
type = string
description = "Max number of CPUs the workspace may use (e.g. 2)."
}
variable "min_cpus" {
type = string
description = "Minimum number of CPUs the workspace may use (e.g. .1)."
}
variable "max_memory" {
type = string
description = "Maximum amount of memory to allocate the workspace (in GB)."
}
variable "min_memory" {
type = string
description = "Minimum amount of memory to allocate the workspace (in GB)."
}
provider "kubernetes" {
# Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences
config_path = var.use_kubeconfig == true ? "~/.kube/config" : null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
startup_script = <<EOT
#!/bin/bash
# home folder can be empty, so copying default bash settings
if [ ! -f ~/.profile ]; then
cp /etc/skel/.profile $HOME
fi
if [ ! -f ~/.bashrc ]; then
cp /etc/skel/.bashrc $HOME
fi
# Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here
EOT
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
resource "kubernetes_persistent_volume_claim" "home" {
metadata {
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
namespace = var.namespace
}
wait_until_bound = false
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${data.coder_parameter.home_disk.value}Gi"
}
}
}
}
resource "kubernetes_pod" "main" {
count = data.coder_workspace.me.start_count
metadata {
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
namespace = var.namespace
}
spec {
restart_policy = "Never"
container {
name = "dev"
# We highly recommend pinning this to a specific release of envbox, as the latest tag may change.
image = "ghcr.io/coder/envbox:latest"
image_pull_policy = "Always"
command = ["/envbox", "docker"]
security_context {
privileged = true
}
resources {
requests = {
"cpu" : "${var.min_cpus}"
"memory" : "${var.min_memory}G"
}
limits = {
"cpu" : "${var.max_cpus}"
"memory" : "${var.max_memory}G"
}
}
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
}
env {
name = "CODER_AGENT_URL"
value = data.coder_workspace.me.access_url
}
env {
name = "CODER_INNER_IMAGE"
value = "index.docker.io/codercom/enterprise-base:ubuntu-20240812"
}
env {
name = "CODER_INNER_USERNAME"
value = "coder"
}
env {
name = "CODER_BOOTSTRAP_SCRIPT"
value = coder_agent.main.init_script
}
env {
name = "CODER_MOUNTS"
value = "/home/coder:/home/coder"
}
env {
name = "CODER_ADD_FUSE"
value = var.create_fuse
}
env {
name = "CODER_INNER_HOSTNAME"
value = data.coder_workspace.me.name
}
env {
name = "CODER_ADD_TUN"
value = var.create_tun
}
env {
name = "CODER_CPUS"
value_from {
resource_field_ref {
resource = "limits.cpu"
}
}
}
env {
name = "CODER_MEMORY"
value_from {
resource_field_ref {
resource = "limits.memory"
}
}
}
volume_mount {
mount_path = "/home/coder"
name = "home"
read_only = false
sub_path = "home"
}
volume_mount {
mount_path = "/var/lib/coder/docker"
name = "home"
sub_path = "cache/docker"
}
volume_mount {
mount_path = "/var/lib/coder/containers"
name = "home"
sub_path = "cache/containers"
}
volume_mount {
mount_path = "/var/lib/sysbox"
name = "sysbox"
}
volume_mount {
mount_path = "/var/lib/containers"
name = "home"
sub_path = "envbox/containers"
}
volume_mount {
mount_path = "/var/lib/docker"
name = "home"
sub_path = "envbox/docker"
}
volume_mount {
mount_path = "/usr/src"
name = "usr-src"
}
volume_mount {
mount_path = "/lib/modules"
name = "lib-modules"
}
}
volume {
name = "home"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
read_only = false
}
}
volume {
name = "sysbox"
empty_dir {}
}
volume {
name = "usr-src"
host_path {
path = "/usr/src"
type = ""
}
}
volume {
name = "lib-modules"
host_path {
path = "/lib/modules"
type = ""
}
}
}
}
@@ -0,0 +1,38 @@
---
display_name: Kubernetes (Deployment)
description: Provision Kubernetes Deployments as Coder workspaces
icon: ../../../../.icons/kubernetes.svg
maintainer_github: coder
verified: true
tags: [kubernetes, container]
---
# Remote Development on Kubernetes Pods
Provision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
### Infrastructure
**Cluster**: This template requires an existing Kubernetes cluster
**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself.
### Authentication
This template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template.
## Architecture
This template provisions the following resources:
- Kubernetes pod (ephemeral)
- Kubernetes persistent volume claim (persistent on `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
+345
View File
@@ -0,0 +1,345 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
provider "coder" {
}
variable "use_kubeconfig" {
type = bool
description = <<-EOF
Use host kubeconfig? (true/false)
Set this to false if the Coder host is itself running as a Pod on the same
Kubernetes cluster as you are deploying workspaces to.
Set this to true if the Coder host is running outside the Kubernetes cluster
for workspaces. A valid "~/.kube/config" must be present on the Coder host.
EOF
default = false
}
variable "namespace" {
type = string
description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace."
}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "The number of CPU cores"
default = "2"
icon = "/icon/memory.svg"
mutable = true
option {
name = "2 Cores"
value = "2"
}
option {
name = "4 Cores"
value = "4"
}
option {
name = "6 Cores"
value = "6"
}
option {
name = "8 Cores"
value = "8"
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory"
description = "The amount of memory in GB"
default = "2"
icon = "/icon/memory.svg"
mutable = true
option {
name = "2 GB"
value = "2"
}
option {
name = "4 GB"
value = "4"
}
option {
name = "6 GB"
value = "6"
}
option {
name = "8 GB"
value = "8"
}
}
data "coder_parameter" "home_disk_size" {
name = "home_disk_size"
display_name = "Home disk size"
description = "The size of the home disk in GB"
default = "10"
type = "number"
icon = "/emojis/1f4be.png"
mutable = false
validation {
min = 1
max = 99999
}
}
provider "kubernetes" {
# Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences
config_path = var.use_kubeconfig == true ? "~/.kube/config" : null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
startup_script = <<-EOT
set -e
# Install the latest code-server.
# Append "--version x.x.x" to install a specific version of code-server.
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
# Start code-server in the background.
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
EOT
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic resources, you can use the `coder stat` command.
# If you need more control, you can write your own script.
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
metadata {
display_name = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
metadata {
display_name = "CPU Usage (Host)"
key = "4_cpu_usage_host"
script = "coder stat cpu --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Memory Usage (Host)"
key = "5_mem_usage_host"
script = "coder stat mem --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Load Average (Host)"
key = "6_load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
}
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
url = "http://localhost:13337?folder=/home/coder"
subdomain = false
share = "owner"
healthcheck {
url = "http://localhost:13337/healthz"
interval = 3
threshold = 10
}
}
resource "kubernetes_persistent_volume_claim" "home" {
metadata {
name = "coder-${data.coder_workspace.me.id}-home"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-pvc"
"app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
//Coder-specific labels.
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
wait_until_bound = false
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${data.coder_parameter.home_disk_size.value}Gi"
}
}
}
}
resource "kubernetes_deployment" "main" {
count = data.coder_workspace.me.start_count
depends_on = [
kubernetes_persistent_volume_claim.home
]
wait_for_rollout = false
metadata {
name = "coder-${data.coder_workspace.me.id}"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
spec {
replicas = 1
selector {
match_labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
}
strategy {
type = "Recreate"
}
template {
metadata {
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
}
spec {
security_context {
run_as_user = 1000
fs_group = 1000
run_as_non_root = true
}
container {
name = "dev"
image = "codercom/enterprise-base:ubuntu"
image_pull_policy = "Always"
command = ["sh", "-c", coder_agent.main.init_script]
security_context {
run_as_user = "1000"
}
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
}
resources {
requests = {
"cpu" = "250m"
"memory" = "512Mi"
}
limits = {
"cpu" = "${data.coder_parameter.cpu.value}"
"memory" = "${data.coder_parameter.memory.value}Gi"
}
}
volume_mount {
mount_path = "/home/coder"
name = "home"
read_only = false
}
}
volume {
name = "home"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
read_only = false
}
}
affinity {
// This affinity attempts to spread out all workspace pods evenly across
// nodes.
pod_anti_affinity {
preferred_during_scheduling_ignored_during_execution {
weight = 1
pod_affinity_term {
topology_key = "kubernetes.io/hostname"
label_selector {
match_expressions {
key = "app.kubernetes.io/name"
operator = "In"
values = ["coder-workspace"]
}
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,103 @@
---
display_name: Nomad
description: Provision Nomad Jobs as Coder workspaces
icon: ../../../../.icons/nomad.svg
maintainer_github: coder
verified: true
tags: [nomad, container]
---
# Remote Development on Nomad
Provision Nomad Jobs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.
<!-- TODO: Add screenshot -->
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
## Prerequisites
- [Nomad](https://www.nomadproject.io/downloads)
- [Docker](https://docs.docker.com/get-docker/)
## Setup
### 1. Start the CSI Host Volume Plugin
The CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.
1. Login to the Nomad server using SSH.
2. Append the following stanza to your Nomad server configuration file and restart the nomad service.
```tf
plugin "docker" {
config {
allow_privileged = true
}
}
```
```shell
sudo systemctl restart nomad
```
3. Create a file `hostpath.nomad` with following content:
```tf
job "hostpath-csi-plugin" {
datacenters = ["dc1"]
type = "system"
group "csi" {
task "plugin" {
driver = "docker"
config {
image = "registry.k8s.io/sig-storage/hostpathplugin:v1.10.0"
args = [
"--drivername=csi-hostpath",
"--v=5",
"--endpoint=${CSI_ENDPOINT}",
"--nodeid=node-${NOMAD_ALLOC_INDEX}",
]
privileged = true
}
csi_plugin {
id = "hostpath"
type = "monolith"
mount_dir = "/csi"
}
resources {
cpu = 256
memory = 128
}
}
}
}
```
4. Run the job:
```shell
nomad job run hostpath.nomad
```
### 2. Setup the Nomad Template
1. Create the template by running the following command:
```shell
coder template init nomad-docker
cd nomad-docker
coder template push
```
2. Set up Nomad server address and optional authentication:
3. Create a new workspace and start developing.
@@ -0,0 +1,193 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
nomad = {
source = "hashicorp/nomad"
}
}
}
variable "nomad_provider_address" {
type = string
description = "Nomad provider address. e.g., http://IP:PORT"
default = "http://localhost:4646"
}
variable "nomad_provider_http_auth" {
type = string
description = "Nomad provider http_auth in the form of `user:password`"
sensitive = true
default = ""
}
provider "coder" {}
provider "nomad" {
address = var.nomad_provider_address
http_auth = var.nomad_provider_http_auth == "" ? null : var.nomad_provider_http_auth
# Fix reading the NOMAD_NAMESPACE and the NOMAD_REGION env var from the coder's allocation.
ignore_env_vars = {
"NOMAD_NAMESPACE" = true
"NOMAD_REGION" = true
}
}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "The number of CPU cores"
default = "1"
icon = "/icon/memory.svg"
mutable = true
option {
name = "1 Cores"
value = "1"
}
option {
name = "2 Cores"
value = "2"
}
option {
name = "3 Cores"
value = "3"
}
option {
name = "4 Cores"
value = "4"
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory"
description = "The amount of memory in GB"
default = "2"
icon = "/icon/memory.svg"
mutable = true
option {
name = "2 GB"
value = "2"
}
option {
name = "4 GB"
value = "4"
}
option {
name = "6 GB"
value = "6"
}
option {
name = "8 GB"
value = "8"
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_agent" "main" {
os = "linux"
arch = "amd64"
startup_script = <<-EOT
set -e
# install and start code-server
curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server
/tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
EOT
metadata {
display_name = "Load Average (Host)"
key = "load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
}
# See https://registry.coder.com/modules/coder/code-server
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
locals {
workspace_tag = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
home_volume_name = "coder_${data.coder_workspace.me.id}_home"
}
resource "nomad_namespace" "coder_workspace" {
name = local.workspace_tag
description = "Coder workspace"
meta = {
owner = data.coder_workspace_owner.me.name
}
}
data "nomad_plugin" "hostpath" {
plugin_id = "hostpath"
wait_for_healthy = true
}
resource "nomad_csi_volume" "home_volume" {
depends_on = [data.nomad_plugin.hostpath]
lifecycle {
ignore_changes = all
}
plugin_id = "hostpath"
volume_id = local.home_volume_name
name = local.home_volume_name
namespace = nomad_namespace.coder_workspace.name
capability {
access_mode = "single-node-writer"
attachment_mode = "file-system"
}
mount_options {
fs_type = "ext4"
}
}
resource "nomad_job" "workspace" {
count = data.coder_workspace.me.start_count
depends_on = [nomad_csi_volume.home_volume]
jobspec = templatefile("${path.module}/workspace.nomad.tpl", {
coder_workspace_owner = data.coder_workspace_owner.me.name
coder_workspace_name = data.coder_workspace.me.name
workspace_tag = local.workspace_tag
cores = tonumber(data.coder_parameter.cpu.value)
memory_mb = tonumber(data.coder_parameter.memory.value * 1024)
coder_init_script = coder_agent.main.init_script
coder_agent_token = coder_agent.main.token
workspace_name = data.coder_workspace.me.name
home_volume_name = local.home_volume_name
})
deregister_on_destroy = true
purge_on_destroy = true
}
resource "coder_metadata" "workspace_info" {
count = data.coder_workspace.me.start_count
resource_id = nomad_job.workspace[0].id
item {
key = "CPU (Cores)"
value = data.coder_parameter.cpu.value
}
item {
key = "Memory (GiB)"
value = data.coder_parameter.memory.value
}
}
@@ -0,0 +1,53 @@
job "workspace" {
datacenters = ["dc1"]
namespace = "${workspace_tag}"
type = "service"
group "workspace" {
volume "home_volume" {
type = "csi"
source = "${home_volume_name}"
read_only = false
attachment_mode = "file-system"
access_mode = "single-node-writer"
}
network {
port "http" {}
}
task "workspace" {
driver = "docker"
config {
image = "codercom/enterprise-base:ubuntu"
ports = ["http"]
labels {
name = "${workspace_tag}"
managed_by = "coder"
}
hostname = "${workspace_name}"
entrypoint = ["sh", "-c", "sudo chown coder:coder -R /home/coder && echo '${base64encode(coder_init_script)}' | base64 --decode | sh"]
}
volume_mount {
volume = "home_volume"
destination = "/home/coder"
}
resources {
cores = ${cores}
memory = ${memory_mb}
}
env {
CODER_AGENT_TOKEN = "${coder_agent_token}"
}
meta {
tag = "${workspace_tag}"
managed_by = "coder"
}
}
meta {
tag = "${workspace_tag}"
managed_by = "coder"
}
}
meta {
tag = "${workspace_tag}"
managed_by = "coder"
}
}
@@ -0,0 +1,12 @@
---
display_name: Scratch
description: A minimal starter template for Coder
icon: ../../../../.icons/box-emoji.svg
maintainer_github: coder
verified: true
tags: []
---
# A minimal Scaffolding for a Coder Template
Use this starter template as a basis to create your own unique template from scratch.
+66
View File
@@ -0,0 +1,66 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = data.coder_provisioner.me.os
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = "coder stat cpu"
interval = 10
timeout = 1
}
metadata {
display_name = "RAM Usage"
key = "1_ram_usage"
script = "coder stat mem"
interval = 10
timeout = 1
}
}
# Use this to set environment variables in your workspace
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env
resource "coder_env" "welcome_message" {
agent_id = coder_agent.main.id
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
}
# Adds code-server
# See all available modules at https://registry.coder.com/modules
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
}
# Runs a script at workspace start/stop or on a cron schedule
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
resource "coder_script" "startup_script" {
agent_id = coder_agent.main.id
display_name = "Startup Script"
script = <<-EOF
#!/bin/sh
set -e
# Run programs at workspace startup
EOF
run_on_start = true
start_blocks_login = true
}
+3 -3
View File
@@ -19,11 +19,11 @@ main() {
# relative to the main script directory
local registry_dir="$script_dir/../registry"
# Get all subdirectories in the registry directory. Code assumes that
# Terraform directories won't begin to appear until three levels deep into
# Get all module subdirectories in the registry directory. Code assumes that
# Terraform module directories won't begin to appear until three levels deep into
# the registry (e.g., registry/coder/modules/coder-login, which will then
# have a main.tf file inside it)
local subdirs=$(find "$registry_dir" -mindepth 3 -type d | sort)
local subdirs=$(find "$registry_dir" -mindepth 3 -path "*/modules/*" -type d | sort)
for dir in $subdirs; do
# Skip over any directories that obviously don't have the necessary
+44 -6
View File
@@ -30,6 +30,21 @@ export const runContainer = async (
return containerID.trim();
};
export const removeContainer = async (id: string) => {
const proc = spawn(["docker", "rm", "-f", id], {
stderr: "pipe",
stdout: "pipe",
});
const exitCode = await proc.exited;
const [stderr, stdout] = await Promise.all([
readableStreamToText(proc.stderr ?? new ReadableStream()),
readableStreamToText(proc.stdout ?? new ReadableStream()),
]);
if (exitCode !== 0) {
throw new Error(`${stderr}\n${stdout}`);
}
};
export interface scriptOutput {
exitCode: number;
stdout: string[];
@@ -279,10 +294,33 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
};
export const writeCoder = async (id: string, script: string) => {
const exec = await execContainer(id, [
"sh",
"-c",
`echo '${script}' > /usr/bin/coder && chmod +x /usr/bin/coder`,
]);
expect(exec.exitCode).toBe(0);
await writeFileContainer(id, "/usr/bin/coder", script, {
user: "root",
});
const execResult = await execContainer(
id,
["chmod", "755", "/usr/bin/coder"],
["--user", "root"],
);
expect(execResult.exitCode).toBe(0);
};
export const writeFileContainer = async (
id: string,
path: string,
content: string,
options?: {
user?: string;
},
) => {
const contentBase64 = Buffer.from(content).toString("base64");
const proc = await execContainer(
id,
["sh", "-c", `echo '${contentBase64}' | base64 -d > '${path}'`],
options?.user ? ["--user", options.user] : undefined,
);
if (proc.exitCode !== 0) {
throw new Error(`Failed to write file: ${proc.stderr}`);
}
expect(proc.exitCode).toBe(0);
};