From e9870049bbca6f36368f755069f26b29716e0fb4 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Wed, 4 Jun 2025 22:10:13 -0700 Subject: [PATCH 1/7] chore: deploy only on tag pushes (#139) Resolves #68 --- .github/workflows/deploy-registry.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy-registry.yaml b/.github/workflows/deploy-registry.yaml index ac3e9d6e..c9e7bcf5 100644 --- a/.github/workflows/deploy-registry.yaml +++ b/.github/workflows/deploy-registry.yaml @@ -2,8 +2,6 @@ name: deploy-registry on: push: - branches: - - main tags: # Matches release/// # (e.g., "release/whizus/exoscale-zone/v1.0.13") From 7cf60c4e5908f21c8eab53c8dc80d3eba75e69c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 14:00:06 +0500 Subject: [PATCH 2/7] chore(deps): bump crate-ci/typos from 1.32.0 to 1.33.1 (#142) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bbbdc06a..a881a05e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,7 @@ jobs: - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.33.1 with: config: .github/typos.toml validate-readme-files: From f5bf6687e76a34d32ec161876272def8aa8cf919 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:17:39 -0500 Subject: [PATCH 3/7] feat: extract version bump logic into reusable script (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Extract Version Bump Logic into Reusable Script This PR extracts the version bump logic from the GitHub Actions workflow (PR #137) into a reusable script and implements the requirements as requested. ## โœ… What This PR Delivers ### ๐Ÿ”ง **Version Bump Script**: `.github/scripts/version-bump.sh` - Extracts all version bump logic from the original workflow - Supports `patch`, `minor`, and `major` version bumps - Configurable base reference for diff comparison (defaults to `origin/main`) - Comprehensive error handling and semantic version validation - Can be used standalone or in workflows ### ๐Ÿ” **Version Check Workflow**: `.github/workflows/version-check.yaml` - **Required CI check** that runs on all PRs modifying modules - Verifies that module versions have been properly updated - Fails if versions need bumping but haven't been updated - Provides clear instructions on how to fix version issues ### ๐Ÿš€ **Version Bump Workflow**: `.github/workflows/version-bump.yaml` - Simplified workflow that uses the extracted script - Triggered by PR labels (`version:patch`, `version:minor`, `version:major`) - Automatically commits version updates and comments on PR ### ๐Ÿ“š **Updated Documentation**: `CONTRIBUTING.md` - Clear instructions on how to use the version bump script - Examples for different bump types - Information about PR labels as an alternative - Explains that CI will check version updates ## ๐ŸŽฏ Key Features โœ… **Script Logic Extracted**: All complex bash logic moved from workflow to reusable script โœ… **Required CI Check**: Version check workflow ensures versions are updated โœ… **Diff Verification**: Script checks git diff to detect modified modules โœ… **Contribution Docs Updated**: Clear instructions for contributors โœ… **Backward Compatible**: Maintains all original functionality โœ… **Error Handling**: Comprehensive validation and clear error messages ## ๐Ÿ“– Usage Examples ```bash # For bug fixes ./.github/scripts/version-bump.sh patch # For new features ./.github/scripts/version-bump.sh minor # For breaking changes ./.github/scripts/version-bump.sh major ``` ## ๐Ÿ”„ Workflow Integration 1. **Developer makes changes** to modules 2. **CI runs version-check** workflow automatically 3. **If versions need updating**, CI fails with instructions 4. **Developer runs script** or adds PR label 5. **Versions get updated** automatically 6. **CI passes** and PR can be merged ## ๐Ÿงช Testing The script has been tested with: - โœ… Valid and invalid bump types - โœ… Module detection from git diff - โœ… Version calculation and validation - โœ… README version updates - โœ… Error handling for edge cases This implementation addresses all the original requirements while making the logic more maintainable and reusable. --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: DevelopmentCats --- .github/scripts/version-bump.sh | 229 ++++++++++++++++++++++++++++ .github/workflows/version-bump.yaml | 107 +++++++++++++ CONTRIBUTING.md | 26 +++- 3 files changed, 360 insertions(+), 2 deletions(-) create mode 100755 .github/scripts/version-bump.sh create mode 100644 .github/workflows/version-bump.yaml diff --git a/.github/scripts/version-bump.sh b/.github/scripts/version-bump.sh new file mode 100755 index 00000000..b074583d --- /dev/null +++ b/.github/scripts/version-bump.sh @@ -0,0 +1,229 @@ +#!/bin/bash + +# Version Bump Script +# Usage: ./version-bump.sh [base_ref] +# bump_type: patch, minor, or major +# base_ref: base reference for diff (default: origin/main) + +set -euo pipefail + +usage() { + echo "Usage: $0 [base_ref]" + echo " bump_type: patch, minor, or major" + echo " base_ref: base reference for diff (default: origin/main)" + echo "" + echo "Examples:" + echo " $0 patch # Update versions with patch bump" + echo " $0 minor # Update versions with minor bump" + echo " $0 major # Update versions with major bump" + exit 1 +} + +validate_version() { + local version="$1" + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "โŒ Invalid version format: '$version'. Expected X.Y.Z format." >&2 + return 1 + fi + return 0 +} + +bump_version() { + local current_version="$1" + local bump_type="$2" + + IFS='.' read -r major minor patch <<< "$current_version" + + if ! [[ "$major" =~ ^[0-9]+$ ]] || ! [[ "$minor" =~ ^[0-9]+$ ]] || ! [[ "$patch" =~ ^[0-9]+$ ]]; then + echo "โŒ Version components must be numeric: major='$major' minor='$minor' patch='$patch'" >&2 + return 1 + fi + + case "$bump_type" in + "patch") + echo "$major.$minor.$((patch + 1))" + ;; + "minor") + echo "$major.$((minor + 1)).0" + ;; + "major") + echo "$((major + 1)).0.0" + ;; + *) + echo "โŒ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2 + return 1 + ;; + esac +} + +update_readme_version() { + local readme_path="$1" + local namespace="$2" + local module_name="$3" + local new_version="$4" + + if [ ! -f "$readme_path" ]; then + return 1 + fi + + local module_source="registry.coder.com/${namespace}/${module_name}/coder" + if grep -q "source.*${module_source}" "$readme_path"; then + echo "Updating version references for $namespace/$module_name in $readme_path" + awk -v module_source="$module_source" -v new_version="$new_version" ' + /source.*=.*/ { + if ($0 ~ module_source) { + in_target_module = 1 + } else { + in_target_module = 0 + } + } + /version.*=.*"/ { + if (in_target_module) { + gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"") + in_target_module = 0 + } + } + { print } + ' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path" + return 0 + elif grep -q 'version\s*=\s*"' "$readme_path"; then + echo "โš ๏ธ Found version references but no module source match for $namespace/$module_name" + return 1 + fi + + return 1 +} + +main() { + if [ $# -lt 1 ] || [ $# -gt 2 ]; then + usage + fi + + local bump_type="$1" + local base_ref="${2:-origin/main}" + + case "$bump_type" in + "patch" | "minor" | "major") ;; + + *) + echo "โŒ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2 + exit 1 + ;; + esac + + echo "๐Ÿ” Detecting modified modules..." + + local changed_files + changed_files=$(git diff --name-only "${base_ref}"...HEAD) + local modules + modules=$(echo "$changed_files" | grep -E '^registry/[^/]+/modules/[^/]+/' | cut -d'/' -f1-4 | sort -u) + + if [ -z "$modules" ]; then + echo "โŒ No modules detected in changes" + exit 1 + fi + + echo "Found modules:" + echo "$modules" + echo "" + + local bumped_modules="" + local updated_readmes="" + local untagged_modules="" + local has_changes=false + + while IFS= read -r module_path; do + if [ -z "$module_path" ]; then continue; fi + + local namespace + namespace=$(echo "$module_path" | cut -d'/' -f2) + local module_name + module_name=$(echo "$module_path" | cut -d'/' -f4) + + echo "๐Ÿ“ฆ Processing: $namespace/$module_name" + + local latest_tag + latest_tag=$(git tag -l "release/${namespace}/${module_name}/v*" | sort -V | tail -1) + local readme_path="$module_path/README.md" + local current_version + + if [ -z "$latest_tag" ]; then + if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then + local readme_version + readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/') + echo "No git tag found, but README shows version: $readme_version" + + if ! validate_version "$readme_version"; then + echo "Starting from v1.0.0 instead" + current_version="1.0.0" + else + current_version="$readme_version" + untagged_modules="$untagged_modules\n- $namespace/$module_name (README: v$readme_version)" + fi + else + echo "No existing tags or version references found for $namespace/$module_name, starting from v1.0.0" + current_version="1.0.0" + fi + else + current_version=$(echo "$latest_tag" | sed 's/.*\/v//') + echo "Found git tag: $latest_tag (v$current_version)" + fi + + echo "Current version: $current_version" + + if ! validate_version "$current_version"; then + exit 1 + fi + + local new_version + new_version=$(bump_version "$current_version" "$bump_type") + + echo "New version: $new_version" + + if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then + updated_readmes="$updated_readmes\n- $namespace/$module_name" + has_changes=true + fi + + bumped_modules="$bumped_modules\n- $namespace/$module_name: v$current_version โ†’ v$new_version" + echo "" + + done <<< "$modules" + + echo "๐Ÿ“‹ Summary:" + echo "Bump Type: $bump_type" + echo "" + echo "Modules Updated:" + echo -e "$bumped_modules" + echo "" + + if [ -n "$updated_readmes" ]; then + echo "READMEs Updated:" + echo -e "$updated_readmes" + echo "" + fi + + if [ -n "$untagged_modules" ]; then + echo "โš ๏ธ Modules Without Git Tags:" + echo -e "$untagged_modules" + echo "These modules were versioned based on README content. Consider creating proper release tags after merging." + echo "" + fi + + if [ "$has_changes" = true ]; then + echo "โœ… Version bump completed successfully!" + echo "๐Ÿ“ README files have been updated with new versions." + echo "" + echo "Next steps:" + echo "1. Review the changes: git diff" + echo "2. Commit the changes: git add . && git commit -m 'chore: bump module versions ($bump_type)'" + echo "3. Push the changes: git push" + exit 0 + else + echo "โ„น๏ธ No README files were updated (no version references found matching module sources)." + echo "Version calculations completed, but no files were modified." + exit 0 + fi +} + +main "$@" diff --git a/.github/workflows/version-bump.yaml b/.github/workflows/version-bump.yaml new file mode 100644 index 00000000..537830a5 --- /dev/null +++ b/.github/workflows/version-bump.yaml @@ -0,0 +1,107 @@ +name: Version Bump + +on: + pull_request: + types: [labeled] + paths: + - "registry/**/modules/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + version-bump: + if: github.event.label.name == 'version:patch' || github.event.label.name == 'version:minor' || github.event.label.name == 'version:major' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract bump type from label + id: bump-type + run: | + case "${{ github.event.label.name }}" in + "version:patch") + echo "type=patch" >> $GITHUB_OUTPUT + ;; + "version:minor") + echo "type=minor" >> $GITHUB_OUTPUT + ;; + "version:major") + echo "type=major" >> $GITHUB_OUTPUT + ;; + *) + echo "Invalid version label: ${{ github.event.label.name }}" + exit 1 + ;; + esac + + - 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" + else + echo "Script failed" + cat "$output_file" + exit 1 + fi + + # Store output for PR comment + { + echo "output<> $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" + else + echo "versions_up_to_date=false" >> $GITHUB_OUTPUT + echo "โŒ Module versions need to be updated" + echo "Files that would be changed:" + git diff --name-only + echo "" + echo "Diff preview:" + git diff + exit 1 + fi + + - name: Comment on PR - Failure + if: failure() && steps.version-check.outputs.versions_up_to_date == 'false' + uses: actions/github-script@v7 + with: + script: | + const output = `${{ steps.version-check.outputs.output }}`; + const bumpType = `${{ steps.bump-type.outputs.type }}`; + + let comment = `## โŒ Version Bump Validation Failed\n\n`; + comment += `**Bump Type:** \`${bumpType}\`\n\n`; + comment += `Module versions need to be updated but haven't been bumped yet.\n\n`; + comment += `**Required Actions:**\n`; + comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`; + comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`; + comment += `3. Push the changes: \`git push\`\n\n`; + comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`; + comment += `> Please update the module versions and push the changes to continue.`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0e8f633..8d63b42d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,13 +258,35 @@ All README files must follow these rules: ## Versioning Guidelines -After your PR is merged, maintainers will handle the release. Understanding version numbers helps you describe the impact of your changes: +When you modify a module, you need to update its version number in the README. Understanding version numbers helps you describe the impact of your changes: - **Patch** (1.2.3 โ†’ 1.2.4): Bug fixes - **Minor** (1.2.3 โ†’ 1.3.0): New features, adding inputs - **Major** (1.2.3 โ†’ 2.0.0): Breaking changes (removing inputs, changing types) -**Important**: Always specify the version change in your PR (e.g., `v1.2.3 โ†’ v1.2.4`). This helps maintainers create the correct release tag. +### Updating Module Versions + +If your changes require a version bump, use the version bump script: + +```bash +# For bug fixes +./.github/scripts/version-bump.sh patch + +# For new features +./.github/scripts/version-bump.sh minor + +# For breaking changes +./.github/scripts/version-bump.sh major +``` + +The script will: + +1. Detect which modules you've modified +2. Calculate the new version number +3. Update all version references in the module's README +4. Show you a summary of changes + +**Important**: Only run the version bump script if your changes require a new release. Documentation-only changes don't need version updates. --- From e54ceb3b92c30fd4ed068dcae7d5cfc2b54588a5 Mon Sep 17 00:00:00 2001 From: DevCats Date: Tue, 10 Jun 2025 21:18:18 -0500 Subject: [PATCH 4/7] fix: replace broken multi-template PR system with unified template (#143) Unifies the broken Multi-PR Template I introduced. --- .github/PULL_REQUEST_TEMPLATE.md | 41 ++++++++++++++----- .github/PULL_REQUEST_TEMPLATE/bug_fix.md | 22 ---------- .../PULL_REQUEST_TEMPLATE/documentation.md | 23 ----------- .github/PULL_REQUEST_TEMPLATE/feature.md | 32 --------------- .github/PULL_REQUEST_TEMPLATE/new_module.md | 25 ----------- 5 files changed, 31 insertions(+), 112 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_fix.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/documentation.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/feature.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/new_module.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9c13c11d..5dc2e84b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,39 @@ -## Choose a PR Template +## Description -Please select the appropriate PR template for your contribution: - -- ๐Ÿ†• [New Module](?template=new_module.md) - Adding a new module to the registry -- ๐Ÿ› [Bug Fix](?template=bug_fix.md) - Fixing an existing issue -- โœจ [Feature](?template=feature.md) - Adding new functionality to a module -- ๐Ÿ“ [Documentation](?template=documentation.md) - Improving docs only + --- -If you've already started your PR, add `?template=TEMPLATE_NAME.md` to your URL. +## Type of Change -### Quick Checklist +- [ ] New module +- [ ] Bug fix +- [ ] Feature/enhancement +- [ ] Documentation +- [ ] Other + +--- + +## Module Information + + + +**Path:** `registry/[namespace]/modules/[module-name]` +**New version:** `v1.0.0` +**Breaking change:** [ ] Yes [ ] No + +--- + +## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun run fmt`) -- [ ] Following contribution guidelines +- [ ] Changes tested locally + +--- + +## Related Issues + + + +Closes # diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md deleted file mode 100644 index c9a53217..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ /dev/null @@ -1,22 +0,0 @@ -## Bug Fix: [Brief Description] - -**Module:** `registry/[namespace]/modules/[module-name]` -**Version:** `v1.2.3` โ†’ `v1.2.4` - -### Problem - - - -### Solution - - - -### Testing - -- [ ] Tests pass (`bun test`) -- [ ] Code formatted (`bun run fmt`) -- [ ] Fix verified locally - -### Related Issue - - diff --git a/.github/PULL_REQUEST_TEMPLATE/documentation.md b/.github/PULL_REQUEST_TEMPLATE/documentation.md deleted file mode 100644 index 32031022..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/documentation.md +++ /dev/null @@ -1,23 +0,0 @@ -## Documentation Update - -### Description - - - -### Changes - - - -- -- - -### Checklist - -- [ ] README validation passes (if applicable) -- [ ] Code examples tested -- [ ] Links verified -- [ ] Formatting consistent - -### Context - - diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md deleted file mode 100644 index b666cc99..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/feature.md +++ /dev/null @@ -1,32 +0,0 @@ -## Feature: [Brief Description] - -**Module:** `registry/[namespace]/modules/[module-name]` -**Version:** `v1.2.3` โ†’ `v1.3.0` (or `v2.0.0` for breaking changes) - -### Description - - - -### Changes - - - -- -- - -### Breaking Changes - - - -- None / _Remove if not applicable_ - -### Testing - -- [ ] Tests pass (`bun test`) -- [ ] Code formatted (`bun run fmt`) -- [ ] New tests added for feature -- [ ] Backward compatibility maintained - -### Documentation - - diff --git a/.github/PULL_REQUEST_TEMPLATE/new_module.md b/.github/PULL_REQUEST_TEMPLATE/new_module.md deleted file mode 100644 index dd0f8427..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/new_module.md +++ /dev/null @@ -1,25 +0,0 @@ -## New Module: [Module Name] - -**Module Path:** `registry/[namespace]/modules/[module-name]` -**Initial Version:** `v1.0.0` - -### Description - - - -### Checklist - -- [ ] All required files included (`main.tf`, `main.test.ts`, `README.md`) -- [ ] Tests pass (`bun test`) -- [ ] Code formatted (`bun run fmt`) -- [ ] README has proper frontmatter -- [ ] Icon path is valid -- [ ] Namespace has avatar (if first-time contributor) - -### Testing - - - -### Additional Notes - - From 01b70dcbaaa837a09ccc031753b963a3d2ed7ce0 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 22:17:34 +0500 Subject: [PATCH 5/7] feat: add group and order inputs to windows-rdp module (#147) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --- registry/coder/modules/windows-rdp/README.md | 6 +++--- registry/coder/modules/windows-rdp/main.tf | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index 87bd98d7..c353c709 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -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.0.18" + version = "1.1.0" 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.0.18" + version = "1.1.0" 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.0.18" + version = "1.1.0" agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index 10ece09c..b7ed9bcd 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -9,6 +9,18 @@ terraform { } } +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 +} + variable "share" { type = string default = "owner" @@ -68,6 +80,8 @@ resource "coder_app" "windows-rdp" { url = "http://localhost:7171" icon = "/icon/desktop.svg" subdomain = true + order = var.order + group = var.group healthcheck { url = "http://localhost:7171" @@ -78,7 +92,7 @@ resource "coder_app" "windows-rdp" { resource "coder_app" "rdp-docs" { agent_id = var.agent_id - display_name = "Local RDP" + display_name = "Local RDP Docs" slug = "rdp-docs" icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop" From 6d1e99d6ae711882fe44b1d8276b0d9a1991eae9 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 23:14:30 +0500 Subject: [PATCH 6/7] feat: add configurable Devolutions Gateway version for windows-rdp module (#148) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --- registry/coder/modules/windows-rdp/README.md | 19 ++++++++++++++++--- registry/coder/modules/windows-rdp/main.tf | 11 +++++++++-- .../powershell-installation-script.tftpl | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index c353c709..cad96407 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -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.1.0" + version = "1.2.0" 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.1.0" + version = "1.2.0" agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id } @@ -46,12 +46,25 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.1.0" + version = "1.2.0" agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id } ``` +### With Custom Devolutions Gateway Version + +```tf +module "windows_rdp" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/windows-rdp/coder" + version = "1.2.0" + agent_id = resource.coder_agent.main.id + resource_id = resource.aws_instance.dev.id + devolutions_gateway_version = "2025.1.6" # Specify a specific version +} +``` + ## Roadmap - [ ] Test on Microsoft Azure. diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index b7ed9bcd..b610c52c 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -51,14 +51,21 @@ variable "admin_password" { sensitive = true } +variable "devolutions_gateway_version" { + type = string + default = "2025.2.1" + description = "Version of Devolutions Gateway to install. Defaults to the latest available version." +} + resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" icon = "/icon/desktop.svg" script = templatefile("${path.module}/powershell-installation-script.tftpl", { - admin_username = var.admin_username - admin_password = var.admin_password + admin_username = var.admin_username + admin_password = var.admin_password + devolutions_gateway_version = var.devolutions_gateway_version # Wanted to have this be in the powershell template file, but Terraform # doesn't allow recursive calls to the templatefile function. Have to feed diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 1b7ab487..c3053584 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -21,7 +21,7 @@ function Configure-RDP { function Install-DevolutionsGateway { # Define the module name and version $moduleName = "DevolutionsGateway" -$moduleVersion = "2024.1.5" +$moduleVersion = "${devolutions_gateway_version}" # Install the module with the specified version for all users # This requires administrator privileges From 05124309eed674075f724b32608e5f9c897d7732 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:06:46 -0500 Subject: [PATCH 7/7] 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 --- .github/typos.toml | 5 +- .icons/1f4e6.png | Bin 0 -> 5637 bytes .icons/do.png | Bin 0 -> 2009 bytes .icons/docker.png | Bin 0 -> 5864 bytes .icons/k8s.png | Bin 0 -> 5274 bytes .icons/lxc.svg | 21 + .icons/nomad.svg | 2 + .../templates/aws-devcontainer/README.md | 111 +++++ .../aws-devcontainer/architecture.svg | 8 + .../cloud-init/cloud-config.yaml.tftpl | 15 + .../cloud-init/userdata.sh.tftpl | 37 ++ .../coder/templates/aws-devcontainer/main.tf | 331 +++++++++++++ registry/coder/templates/aws-linux/README.md | 94 ++++ .../cloud-init/cloud-config.yaml.tftpl | 8 + .../aws-linux/cloud-init/userdata.sh.tftpl | 2 + registry/coder/templates/aws-linux/main.tf | 296 +++++++++++ .../coder/templates/aws-windows/README.md | 96 ++++ registry/coder/templates/aws-windows/main.tf | 214 ++++++++ .../coder/templates/azure-linux/README.md | 64 +++ .../cloud-init/cloud-config.yaml.tftpl | 56 +++ registry/coder/templates/azure-linux/main.tf | 325 ++++++++++++ .../azure-windows/FirstLogonCommands.xml | 12 + .../azure-windows/Initialize.ps1.tftpl | 73 +++ .../coder/templates/azure-windows/README.md | 64 +++ .../coder/templates/azure-windows/main.tf | 210 ++++++++ .../templates/digitalocean-linux/README.md | 52 ++ .../cloud-config.yaml.tftpl | 46 ++ .../templates/digitalocean-linux/main.tf | 361 ++++++++++++++ .../templates/docker-devcontainer/README.md | 77 +++ .../templates/docker-devcontainer/main.tf | 372 ++++++++++++++ registry/coder/templates/docker/README.md | 48 ++ registry/coder/templates/docker/main.tf | 220 +++++++++ .../templates/gcp-devcontainer/README.md | 80 +++ .../gcp-devcontainer/architecture.svg | 8 + .../coder/templates/gcp-devcontainer/main.tf | 341 +++++++++++++ registry/coder/templates/gcp-linux/README.md | 64 +++ registry/coder/templates/gcp-linux/main.tf | 184 +++++++ .../templates/gcp-vm-container/README.md | 65 +++ .../coder/templates/gcp-vm-container/main.tf | 136 +++++ .../coder/templates/gcp-windows/README.md | 64 +++ registry/coder/templates/gcp-windows/main.tf | 96 ++++ registry/coder/templates/incus/README.md | 51 ++ registry/coder/templates/incus/main.tf | 317 ++++++++++++ .../kubernetes-devcontainer/README.md | 58 +++ .../templates/kubernetes-devcontainer/main.tf | 464 ++++++++++++++++++ .../templates/kubernetes-envbox/README.md | 59 +++ .../coder/templates/kubernetes-envbox/main.tf | 322 ++++++++++++ registry/coder/templates/kubernetes/README.md | 38 ++ registry/coder/templates/kubernetes/main.tf | 345 +++++++++++++ .../coder/templates/nomad-docker/README.md | 103 ++++ registry/coder/templates/nomad-docker/main.tf | 193 ++++++++ .../nomad-docker/workspace.nomad.tpl | 53 ++ registry/coder/templates/scratch/README.md | 12 + registry/coder/templates/scratch/main.tf | 66 +++ scripts/terraform_validate.sh | 6 +- 55 files changed, 6341 insertions(+), 4 deletions(-) create mode 100644 .icons/1f4e6.png create mode 100644 .icons/do.png create mode 100644 .icons/docker.png create mode 100644 .icons/k8s.png create mode 100644 .icons/lxc.svg create mode 100644 .icons/nomad.svg create mode 100644 registry/coder/templates/aws-devcontainer/README.md create mode 100644 registry/coder/templates/aws-devcontainer/architecture.svg create mode 100644 registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl create mode 100644 registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl create mode 100644 registry/coder/templates/aws-devcontainer/main.tf create mode 100644 registry/coder/templates/aws-linux/README.md create mode 100644 registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl create mode 100644 registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl create mode 100644 registry/coder/templates/aws-linux/main.tf create mode 100644 registry/coder/templates/aws-windows/README.md create mode 100644 registry/coder/templates/aws-windows/main.tf create mode 100644 registry/coder/templates/azure-linux/README.md create mode 100644 registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl create mode 100644 registry/coder/templates/azure-linux/main.tf create mode 100644 registry/coder/templates/azure-windows/FirstLogonCommands.xml create mode 100644 registry/coder/templates/azure-windows/Initialize.ps1.tftpl create mode 100644 registry/coder/templates/azure-windows/README.md create mode 100644 registry/coder/templates/azure-windows/main.tf create mode 100644 registry/coder/templates/digitalocean-linux/README.md create mode 100644 registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl create mode 100644 registry/coder/templates/digitalocean-linux/main.tf create mode 100644 registry/coder/templates/docker-devcontainer/README.md create mode 100644 registry/coder/templates/docker-devcontainer/main.tf create mode 100644 registry/coder/templates/docker/README.md create mode 100644 registry/coder/templates/docker/main.tf create mode 100644 registry/coder/templates/gcp-devcontainer/README.md create mode 100644 registry/coder/templates/gcp-devcontainer/architecture.svg create mode 100644 registry/coder/templates/gcp-devcontainer/main.tf create mode 100644 registry/coder/templates/gcp-linux/README.md create mode 100644 registry/coder/templates/gcp-linux/main.tf create mode 100644 registry/coder/templates/gcp-vm-container/README.md create mode 100644 registry/coder/templates/gcp-vm-container/main.tf create mode 100644 registry/coder/templates/gcp-windows/README.md create mode 100644 registry/coder/templates/gcp-windows/main.tf create mode 100644 registry/coder/templates/incus/README.md create mode 100644 registry/coder/templates/incus/main.tf create mode 100644 registry/coder/templates/kubernetes-devcontainer/README.md create mode 100644 registry/coder/templates/kubernetes-devcontainer/main.tf create mode 100644 registry/coder/templates/kubernetes-envbox/README.md create mode 100644 registry/coder/templates/kubernetes-envbox/main.tf create mode 100644 registry/coder/templates/kubernetes/README.md create mode 100644 registry/coder/templates/kubernetes/main.tf create mode 100644 registry/coder/templates/nomad-docker/README.md create mode 100644 registry/coder/templates/nomad-docker/main.tf create mode 100644 registry/coder/templates/nomad-docker/workspace.nomad.tpl create mode 100644 registry/coder/templates/scratch/README.md create mode 100644 registry/coder/templates/scratch/main.tf diff --git a/.github/typos.toml b/.github/typos.toml index 5889c7d7..f27257a2 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -1,4 +1,7 @@ [default.extend-words] muc = "muc" # For Munich location code Hashi = "Hashi" -HashiCorp = "HashiCorp" \ No newline at end of file +HashiCorp = "HashiCorp" + +[files] +extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/.icons/1f4e6.png b/.icons/1f4e6.png new file mode 100644 index 0000000000000000000000000000000000000000..ed5cbdcf39f5c589a6164a7c91eef303f9353662 GIT binary patch literal 5637 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 literal 0 HcmV?d00001 diff --git a/.icons/do.png b/.icons/do.png new file mode 100644 index 0000000000000000000000000000000000000000..827bcaa6446babdc583d9a72f5ce14cf6fac062c GIT binary patch literal 2009 zcmV;~2PXK5P)z14IQ4i3c8k2#FC?LW~FogFr3q?otYhLZBEW-Wmyl zQ9=|DiP9KjlqisJD2It4f+!-&4YtqkZ`uZ=%kF3HjK<&z%(aBtk&&0#4FR(3WB;45b)%@jrbT+QJ)uGrjNl zxt?jR6R^6WSj@eL5oNSxL*9p^+7FL7 z7jTAd!+ye>+`AXqftWeBvvJu64>%LBI_JHE>=Cb)T{L)xW1e|VjHQhsJqpE&H~YjB zda(&yL2Kf27?MUndPv%#j5}kIy$oiZxX5}|15%$UuFpyjlMdoAHFXL!~3nD$zn#%HX5 zoY`cvb!YY~22t31c>bsWdOc^E_RT0!-@7^K(gJENogcB<2f;snkhBq4X zS*AFNcwWGS7F)}!lOV}7%Al0DV&oNR1gbCtUWj3mWsRhbR*SO4y9(wkt;u;+2m`a! z^W3h2Ic-@HjZPP-T0k)es?tYJ!9JYB^m@;Ebr}a40yRZj17j(v&4B~0rrQ+7O1n(` zim;JTM|p2v>casw4XZ7+0^R{rG;PKWcne{r6^9)ZOiigK48u}Is1U%EoZ+bs#+XILS#TrFID|IVKz6ukgg0vJQxgVRZG;UmHXb*@eA>k_O!nv) z$OJ{|AfL1XW^|$WSK1wn@vk4u`4CZ|3ZKA?=BYeBQU~^d8Gqp@wjnCyv8gj9g@q!% z->xuYI~LMRhzeV+AKR?4OW`V$9C!uH*nueVfszTEW)qDRuC_ZQ0=9=)qI(cEG=6{y z+L}FkNn$*B8O#z5-p}Vhhhahr|ET1^#xUbJqQ@zipzSph`>zR1NFZ#EgCMbWYvqT= z{gfG6zzo~Yf)}@U6fw0pY+d`gC7?ognDK|*f!q;rif+bQyEjbGu${e?I|8;uf0#N= zWF3kPxg%gcZi6X#uw8SK8v@Gcn#L%YaRRXKbp^BhN+{O-_{%*Gx5(ncq$+}+BrTt3 zf0&@LljqwuBEbom&;DceIyrYJ{KBoj0s@m^I%h`7EH(Sl#_TV`v-I`cJ^*u4jxA@u zfK_-F)$5Q=IErTYHdTb|7BHbla61O3D14l5Ra5>27aPLuok-1I_MMj?3KVMbbIN$N z>3l(rbC{bh(uIx4K!zD@C!Iiw_>4P#ry39Qxr$>W~;Og*z7C96LcevqphKM zgW&~@1x})6mw?jW?TF90Hw50;f;d(?iI_bC#&stSPJG0i<9+RN4D+4NGz-p<&LpM! z7F1dbA^n)C{|$XF%biA!wtxvOZv#nEJXy!*{aF-bBGOZMRh@HsO7sLw=okg-%VH+; zxgP|4T(P84PBU$zn^U48psY-4jb7|k+{#>XKb;f*f!q~PR*=S3DLj%L zhO~Poa@gQy_&}}=tjo)G9%19=i+DmV<-ko8a6F6cX81_<=RjpmX)DKRj6jLM;tAQF z12?j-oiAE)oBoI06=;04p=K z04PN7k*>jQS58~6OJ2*`d?}p`D>6M{G^#~-A4S_h!PejiKE>kn?=}dEunH&(Vq_A# rtU;EEJ$otG{+eY9o1DIqBoq7x>}}x>+XE)Z00000NkvXXu0mjfw^5^M literal 0 HcmV?d00001 diff --git a/.icons/docker.png b/.icons/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..f07559cbc34d5e1e38fca802d810befb491c55b2 GIT binary patch literal 5864 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 literal 0 HcmV?d00001 diff --git a/.icons/lxc.svg b/.icons/lxc.svg new file mode 100644 index 00000000..0e8e118f --- /dev/null +++ b/.icons/lxc.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.icons/nomad.svg b/.icons/nomad.svg new file mode 100644 index 00000000..b4dc91c7 --- /dev/null +++ b/.icons/nomad.svg @@ -0,0 +1,2 @@ + + diff --git a/registry/coder/templates/aws-devcontainer/README.md b/registry/coder/templates/aws-devcontainer/README.md new file mode 100644 index 00000000..1f944039 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/README.md @@ -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) + + + +## 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). diff --git a/registry/coder/templates/aws-devcontainer/architecture.svg b/registry/coder/templates/aws-devcontainer/architecture.svg new file mode 100644 index 00000000..be66c3f1 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/architecture.svg @@ -0,0 +1,8 @@ +AWSAWSHostingHostingVirtualย MachineVirtualย MachineLinuxย HardwareLinuxย HardwareCoderย WorkspaceCoderย WorkspaceDevcontainerDevcontainerenvbuilderย createdย filesystemenvbuilderย createdย filesystemAย Cloneย ofย yourย repoAย Cloneย ofย yourย repoSourceย codeSourceย codeLanguagesLanguagesPython.ย Go,ย etcPython.ย Go,ย etcToolingToolingExtensions,ย linting,ย formatting,ย etcExtensions,ย linting,ย formatting,ย etcCPUsCPUsDiskย StorageDiskย StorageCodeย EditorCodeย EditorVSย Codeย DesktopVSย Codeย DesktopLocalย InstallationLocalย InstallationVSย Codeย DesktopVSย Codeย DesktopLocalย InstallationLocalย Installationcode-servercode-serverAย webย IDEAย webย IDEJetBrainsย GatewayJetBrainsย GatewayLocalย InstallationLocalย InstallationCommandย LineCommandย LineSSHย viaย Coderย CLISSHย viaย Coderย CLI \ No newline at end of file diff --git a/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 00000000..af6b3517 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl @@ -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 diff --git a/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl b/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl new file mode 100644 index 00000000..67c166cb --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl @@ -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} diff --git a/registry/coder/templates/aws-devcontainer/main.tf b/registry/coder/templates/aws-devcontainer/main.tf new file mode 100644 index 00000000..b23b9a65 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/main.tf @@ -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 +} diff --git a/registry/coder/templates/aws-linux/README.md b/registry/coder/templates/aws-linux/README.md new file mode 100644 index 00000000..248fbea1 --- /dev/null +++ b/registry/coder/templates/aws-linux/README.md @@ -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`. diff --git a/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 00000000..14da7694 --- /dev/null +++ b/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl @@ -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 diff --git a/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl b/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl new file mode 100644 index 00000000..2070bc4d --- /dev/null +++ b/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl @@ -0,0 +1,2 @@ +#!/bin/bash +sudo -u '${linux_user}' sh -c '${init_script}' diff --git a/registry/coder/templates/aws-linux/main.tf b/registry/coder/templates/aws-linux/main.tf new file mode 100644 index 00000000..bf59dadc --- /dev/null +++ b/registry/coder/templates/aws-linux/main.tf @@ -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" +} diff --git a/registry/coder/templates/aws-windows/README.md b/registry/coder/templates/aws-windows/README.md new file mode 100644 index 00000000..0ebd075d --- /dev/null +++ b/registry/coder/templates/aws-windows/README.md @@ -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. + + + +## 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`. diff --git a/registry/coder/templates/aws-windows/main.tf b/registry/coder/templates/aws-windows/main.tf new file mode 100644 index 00000000..167b1b69 --- /dev/null +++ b/registry/coder/templates/aws-windows/main.tf @@ -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 = < +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +${coder_agent.main.init_script} + +true +EOT + + user_data_end = < +shutdown /s + +true +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" + } +} diff --git a/registry/coder/templates/azure-linux/README.md b/registry/coder/templates/azure-linux/README.md new file mode 100644 index 00000000..9b5ba079 --- /dev/null +++ b/registry/coder/templates/azure-linux/README.md @@ -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. + + + +## 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=` +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}" + } +} +``` diff --git a/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 00000000..75006778 --- /dev/null +++ b/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl @@ -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 diff --git a/registry/coder/templates/azure-linux/main.tf b/registry/coder/templates/azure-linux/main.tf new file mode 100644 index 00000000..687c8cae --- /dev/null +++ b/registry/coder/templates/azure-linux/main.tf @@ -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" + } +} diff --git a/registry/coder/templates/azure-windows/FirstLogonCommands.xml b/registry/coder/templates/azure-windows/FirstLogonCommands.xml new file mode 100644 index 00000000..ac4a9d80 --- /dev/null +++ b/registry/coder/templates/azure-windows/FirstLogonCommands.xml @@ -0,0 +1,12 @@ + + + cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1" + Copy Initialize.ps1 to file from CustomData + 3 + + + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log" + Execute Initialize.ps1 script + 4 + + diff --git a/registry/coder/templates/azure-windows/Initialize.ps1.tftpl b/registry/coder/templates/azure-windows/Initialize.ps1.tftpl new file mode 100644 index 00000000..ae1bdef7 --- /dev/null +++ b/registry/coder/templates/azure-windows/Initialize.ps1.tftpl @@ -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" diff --git a/registry/coder/templates/azure-windows/README.md b/registry/coder/templates/azure-windows/README.md new file mode 100644 index 00000000..1fb1bfe1 --- /dev/null +++ b/registry/coder/templates/azure-windows/README.md @@ -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. + + + +## 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=` +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}" + } +} +``` diff --git a/registry/coder/templates/azure-windows/main.tf b/registry/coder/templates/azure-windows/main.tf new file mode 100644 index 00000000..65447a77 --- /dev/null +++ b/registry/coder/templates/azure-windows/main.tf @@ -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 = "${random_password.admin_password.result}true1${local.admin_username}" + 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" +} diff --git a/registry/coder/templates/digitalocean-linux/README.md b/registry/coder/templates/digitalocean-linux/README.md new file mode 100644 index 00000000..5acddc31 --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/README.md @@ -0,0 +1,52 @@ +--- +display_name: DigitalOcean Droplet (Linux) +description: Provision DigitalOcean Droplets as Coder workspaces +icon: ../../../../.icons/do.png +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. + + + +## 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. diff --git a/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl b/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl new file mode 100644 index 00000000..a2400b54 --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl @@ -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 diff --git a/registry/coder/templates/digitalocean-linux/main.tf b/registry/coder/templates/digitalocean-linux/main.tf new file mode 100644 index 00000000..4daf4b8b --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/main.tf @@ -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" + } +} diff --git a/registry/coder/templates/docker-devcontainer/README.md b/registry/coder/templates/docker-devcontainer/README.md new file mode 100644 index 00000000..0c07d5ca --- /dev/null +++ b/registry/coder/templates/docker-devcontainer/README.md @@ -0,0 +1,77 @@ +--- +display_name: Docker (Devcontainer) +description: Provision envbuilder containers as Coder workspaces +icon: ../../../../.icons/docker.png +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. diff --git a/registry/coder/templates/docker-devcontainer/main.tf b/registry/coder/templates/docker-devcontainer/main.tf new file mode 100644 index 00000000..2765874f --- /dev/null +++ b/registry/coder/templates/docker-devcontainer/main.tf @@ -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 = < + +## 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. diff --git a/registry/coder/templates/docker/main.tf b/registry/coder/templates/docker/main.tf new file mode 100644 index 00000000..234c4338 --- /dev/null +++ b/registry/coder/templates/docker/main.tf @@ -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 = < **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. diff --git a/registry/coder/templates/gcp-devcontainer/architecture.svg b/registry/coder/templates/gcp-devcontainer/architecture.svg new file mode 100644 index 00000000..c3dfd645 --- /dev/null +++ b/registry/coder/templates/gcp-devcontainer/architecture.svg @@ -0,0 +1,8 @@ +GCPGCPHostingHostingVirtualย MachineVirtualย MachineLinuxย HardwareLinuxย HardwareCoderย WorkspaceCoderย WorkspaceDevcontainerDevcontainerenvbuilderย createdย filesystemenvbuilderย createdย filesystemAย Cloneย ofย yourย repoAย Cloneย ofย yourย repoSourceย codeSourceย codeLanguagesLanguagesPython.ย Go,ย etcPython.ย Go,ย etcToolingToolingExtensions,ย linting,ย formatting,ย etcExtensions,ย linting,ย formatting,ย etcCPUsCPUsDiskย StorageDiskย StorageCodeย EditorCodeย EditorVSย Codeย DesktopVSย Codeย DesktopLocalย InstallationLocalย InstallationVSย Codeย DesktopVSย Codeย DesktopLocalย InstallationLocalย Installationcode-servercode-serverAย webย IDEAย webย IDEJetBrainsย GatewayJetBrainsย GatewayLocalย InstallationLocalย InstallationCommandย LineCommandย LineSSHย viaย Coderย CLISSHย viaย Coderย CLI \ No newline at end of file diff --git a/registry/coder/templates/gcp-devcontainer/main.tf b/registry/coder/templates/gcp-devcontainer/main.tf new file mode 100644 index 00000000..317a22fc --- /dev/null +++ b/registry/coder/templates/gcp-devcontainer/main.tf @@ -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" + } +} diff --git a/registry/coder/templates/gcp-linux/README.md b/registry/coder/templates/gcp-linux/README.md new file mode 100644 index 00000000..60191040 --- /dev/null +++ b/registry/coder/templates/gcp-linux/README.md @@ -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`. diff --git a/registry/coder/templates/gcp-linux/main.tf b/registry/coder/templates/gcp-linux/main.tf new file mode 100644 index 00000000..286db4e4 --- /dev/null +++ b/registry/coder/templates/gcp-linux/main.tf @@ -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 = </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" + } +} diff --git a/registry/coder/templates/gcp-vm-container/README.md b/registry/coder/templates/gcp-vm-container/README.md new file mode 100644 index 00000000..83704ee2 --- /dev/null +++ b/registry/coder/templates/gcp-vm-container/README.md @@ -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`. diff --git a/registry/coder/templates/gcp-vm-container/main.tf b/registry/coder/templates/gcp-vm-container/main.tf new file mode 100644 index 00000000..b259b4b2 --- /dev/null +++ b/registry/coder/templates/gcp-vm-container/main.tf @@ -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 + } +} diff --git a/registry/coder/templates/gcp-windows/README.md b/registry/coder/templates/gcp-windows/README.md new file mode 100644 index 00000000..ac717e41 --- /dev/null +++ b/registry/coder/templates/gcp-windows/README.md @@ -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`. diff --git a/registry/coder/templates/gcp-windows/main.tf b/registry/coder/templates/gcp-windows/main.tf new file mode 100644 index 00000000..aea409ee --- /dev/null +++ b/registry/coder/templates/gcp-windows/main.tf @@ -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" + } +} diff --git a/registry/coder/templates/incus/README.md b/registry/coder/templates/incus/README.md new file mode 100644 index 00000000..def594cd --- /dev/null +++ b/registry/coder/templates/incus/README.md @@ -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! diff --git a/registry/coder/templates/incus/main.tf b/registry/coder/templates/incus/main.tf new file mode 100644 index 00000000..95e10a6d --- /dev/null +++ b/registry/coder/templates/incus/main.tf @@ -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 = < **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`. diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf new file mode 100644 index 00000000..8fc79fa2 --- /dev/null +++ b/registry/coder/templates/kubernetes-devcontainer/main.tf @@ -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 = < + +## 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. diff --git a/registry/coder/templates/kubernetes/main.tf b/registry/coder/templates/kubernetes/main.tf new file mode 100644 index 00000000..e1fdb12c --- /dev/null +++ b/registry/coder/templates/kubernetes/main.tf @@ -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 = < + +> **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. diff --git a/registry/coder/templates/nomad-docker/main.tf b/registry/coder/templates/nomad-docker/main.tf new file mode 100644 index 00000000..9fc50893 --- /dev/null +++ b/registry/coder/templates/nomad-docker/main.tf @@ -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 = <