Compare commits

..

20 Commits

Author SHA1 Message Date
blink-so[bot] 4ce3cd189a fix: remove extra blank line to pass terraform fmt check
Terraform fmt requires consistent spacing in resource blocks.
2025-09-05 05:44:46 +00:00
blink-so[bot] 41cf2c6fab fix: format README.md to pass CI checks
Remove extra space before comment to match prettier formatting rules.
2025-09-05 05:42:44 +00:00
blink-so[bot] 73996b1c26 feat(agentapi): add enable_tasks variable to conditionally create coder_ai_task resource
This allows users to disable AI task creation when using the agentapi module
with count = 0, avoiding the validation requirement for 'AI Prompt' parameter.

When enable_tasks = false, the coder_ai_task resource will not be created,
allowing the module to be used without defining the required AI Prompt parameter.
2025-09-05 05:39:13 +00:00
Atif Ali 9e47369905 chore: mark AMP CLI agent as verified (#408) 2025-09-03 04:37:22 +00:00
हिमांशु d9d44ca338 fix: bump versions of jfrog-oauth and jfrog-token (#407)
Closes #

## Description

## Type of Change

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

## Testing & Validation

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

## Related Issues
Follow up of #375
2025-09-01 22:31:43 +05:00
हिमांशु 7152b85246 feat: add conda support to JFrog modules (#375) 2025-09-01 21:54:23 +05:00
Atif Ali 41c6bece3e fix: use correct source url (#404) 2025-09-01 12:56:04 +00:00
Marcin Tojek 9452763f7d add: rstudio module (#327) 2025-09-01 14:25:28 +02:00
m4rrypro 77328656ff feat: add linode vm template (#367)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:07:54 +05:00
m4rrypro c4c484089f feat: add digitalocean region module (#355)
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-31 17:06:08 +05:00
blink-so[bot] 7e53098bea Update jetbrains-gateway module references to coder/jetbrains (#396)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-29 17:46:25 +05:00
Lucas Kaplan 901043bb01 chore: Fixed path to avatar (#400)
This PR fixes a path issue in the README of the AJ0070 profile. The
original avatar path had the wrong extension.

---------

Co-authored-by: Benjamin <benjaminpeinhardt@gmail.com>
2025-08-28 18:56:11 -04:00
Jash Ambaliya 35e64f2e4a feat(pgadmin): add new module for pgAdmin (#228)
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 22:32:27 +05:30
blink-so[bot] 65edb54e88 Add template scaffolding script and enhance module script (#395)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: matifali <104543375+matifali@users.noreply.github.com>
Co-authored-by: DevCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 20:42:05 +05:00
blink-so[bot] c270edfdab docs: add Premium license warning for externally-managed-workspace template (#397)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: M Atif Ali <U04T3LN8ASU+AtifCoder@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-08-28 11:14:57 +00:00
Kacper Sawicki f712d1c55b feat: add template for externally managed workspaces to coder-labs (#343)
## Description

Add externally-managed-workspace template for connecting Coder
workspaces to externally provisioned compute resources

## Type of Change

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

## Testing & Validation

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

## Related Issues

https://github.com/coder/coder/issues/19091
2025-08-28 10:33:29 +00:00
Atif Ali bc383a32f3 chore: add AGENTS.md (#393)
## Summary
- Adds comprehensive AGENTS.md documentation for AI coding assistants
- Provides guidance on project structure, development commands, and
testing workflows
- Includes specific instructions for Terraform module development and
validation

## Test plan
- [ ] Validate document formatting and structure
- [ ] Verify all referenced commands work correctly
- [ ] Test that instructions align with existing project workflows

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-08-27 21:02:24 -05:00
blink-so[bot] a9b015044f Update coder-login module to use coder_env resources (#389)
This PR updates the `coder-login` module to use `coder_env` resources
instead of shell scripts for better security, maintainability, and
native Terraform integration.

## Changes
- **Replaced `coder_script` with `coder_env` resources**: Uses native
Terraform provider resources instead of shell scripts
- **Removed `run.sh` script**: Eliminated the need for external shell
scripts
- **Environment variables**: Sets `CODER_SESSION_TOKEN` and `CODER_URL`
using `coder_env` resources
- **Added comprehensive tests**: Includes Terraform tests with mocked
data validation
- **Version bump**: Updated module version from `v1.0.31` to `v1.1.0`
(minor bump)

## Benefits
- **Native Terraform approach**: Uses the provider's built-in resources
instead of external scripts
- **Better security**: Environment variables are set directly by
Terraform without shell script interpolation
- **Improved maintainability**: Cleaner, more declarative configuration
- **Proper testing**: Comprehensive test coverage with mocked data
sources
- **Correct environment variables**: Uses `CODER_SESSION_TOKEN` and
`CODER_URL` as per coder CLI documentation

## Testing
- All Terraform tests pass successfully
- Module validates correctly with `terraform validate`
- Proper formatting verified with `terraform fmt`

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: Cian Johnston <public@cianjohnston.ie>
2025-08-26 21:33:00 -05:00
Michael Smith e94dfd2df6 fix: add validation for Github-Flavored Markdown Alerts (#394)
No issue to link – this was a problem we discovered while updating the
Registry website

## Description

This PR adds (very) basic validation for the GitHub Flavored Markdown
alerts that we allow contributors to add to their README files. The
errors that get generated should be correct, but the error messages
themselves aren't as helpful as they could be. I'm going to be handling
that in a separate PR, just so we can get this one in sooner.

### Changes made
- Added function for validating the core structure of all GFM alerts
- Updated existing README files that were failing the new validation
requirements

## Type of Change

- [ ] New module
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
2025-08-26 20:52:22 -05:00
DevCats 9125a52f57 feat: add mcp config for kiro and windsurf modules (#391)
## Description

- Introduces mcp file creation via `coder_script` in kiro and windsurf
modules
- Add mcp variable to both modules
- Add slug and display_name variables to windsurf to match up with other
modules
- Add tests for testing mcp file creation for both kiro and windsurf

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

**Path:** `registry/coder/modules/kiro`  
**New version:** `v1.1.0`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder/modules/windsurf`  
**New version:** `v1.2.0`  
**Breaking change:** [ ] Yes [X] No

**Path:** `registry/coder/modules/cursor`  
**New version:** `v1.3.2`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally
2025-08-26 20:48:15 -05:00
78 changed files with 2551 additions and 320 deletions
+1
View File
@@ -0,0 +1 @@
../AGENTS.md
+1
View File
@@ -1,5 +1,6 @@
[default.extend-words]
muc = "muc" # For Munich location code
tyo = "tyo" # For Tokyo location code
Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
+4
View File
@@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Akamai</title>
<path d="M13.0548 0C6.384 0 .961 5.3802.961 12.0078.961 18.6354 6.3698 24 13.0548 24c.6168 0 .6454-.3572.0859-.5293-4.9349-1.5063-8.5352-6.069-8.5352-11.4629 0-5.4656 3.6725-10.0706 8.6934-11.5195C13.8153.3448 13.6716 0 13.0548 0Zm2.3242 1.8223c-5.2648 0-9.5254 4.2606-9.5254 9.5254 0 1.2193.2285 2.3818.6445 3.4433.1722.459.4454.4584.4024.0137-.0287-.3156-.0567-.6447-.0567-.9746 0-5.2648 4.2606-9.5254 9.5254-9.5254 4.9779 0 6.4698 2.2235 6.6563 2.08.2008-.1577-1.808-4.5624-7.6465-4.5624zm.4687 4.0703c-1.8622.0592-3.651.7168-5.1035 1.8554-.2582.2009-.1567.3284.1445.1993 2.4675-1.076 5.5812-1.1046 8.6368-.043 2.0514.7173 3.2413 1.7364 3.3418 1.6934.1578-.0718-1.1915-2.2226-3.6446-3.1407-1.1135-.4196-2.2576-.6-3.375-.5644z" fill="#0096D6"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" font-size="96px" text-anchor="middle" dominant-baseline="middle" font-family="Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji, sans-serif">🔌</text></svg>

After

Width:  |  Height:  |  Size: 247 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="64" viewBox="0 0 25.6 25.6" width="64"><style><![CDATA[.B{stroke-linecap:round}.C{stroke-linejoin:round}.D{stroke-linejoin:miter}.E{stroke-width:.716}]]></style><g fill="none" stroke="#fff"><path d="M18.983 18.636c.163-1.357.114-1.555 1.124-1.336l.257.023c.777.035 1.793-.125 2.4-.402 1.285-.596 2.047-1.592.78-1.33-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.227-11.687-3.004-3.84-8.205-2.024-8.292-1.976l-.028.005c-.57-.12-1.2-.19-1.93-.2-1.308-.02-2.3.343-3.054.914 0 0-9.277-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.01 2.01 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.285 1.76.33 2.842s.116 2.093.337 2.688.48 2.13 2.53 1.7c1.713-.367 3.023-.896 3.143-5.81" fill="#000" stroke="#000" stroke-linecap="butt" stroke-width="2.149" class="D"/><path d="M23.535 15.6c-2.89.596-3.1-.383-3.1-.383 3.053-4.53 4.33-10.28 3.228-11.687-3.004-3.84-8.205-2.023-8.292-1.976l-.028.005a10.31 10.31 0 0 0-1.929-.201c-1.308-.02-2.3.343-3.054.914 0 0-9.278-3.822-8.846 4.807.092 1.836 2.63 13.9 5.66 10.25C8.29 15.987 9.36 14.86 9.36 14.86c.53.353 1.167.533 1.834.468l.052-.044a2.02 2.02 0 0 0 .021.518c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.162l-.046.183c.306.245.52 1.593.484 2.815s-.06 2.06.18 2.716.48 2.13 2.53 1.7c1.713-.367 2.6-1.32 2.725-2.906.088-1.128.286-.962.3-1.97l.16-.478c.183-1.53.03-2.023 1.085-1.793l.257.023c.777.035 1.794-.125 2.39-.402 1.285-.596 2.047-1.592.78-1.33z" fill="#336791" stroke="none"/><g class="E"><g class="B"><path d="M12.814 16.467c-.08 2.846.02 5.712.298 6.4s.875 2.05 2.926 1.612c1.713-.367 2.337-1.078 2.607-2.647l.633-5.017M10.356 2.2S1.072-1.596 1.504 7.033c.092 1.836 2.63 13.9 5.66 10.25C8.27 15.95 9.27 14.907 9.27 14.907m6.1-13.4c-.32.1 5.164-2.005 8.282 1.978 1.1 1.407-.175 7.157-3.228 11.687" class="C"/><path d="M20.425 15.17s.2.98 3.1.382c1.267-.262.504.734-.78 1.33-1.054.49-3.418.615-3.457-.06-.1-1.745 1.244-1.215 1.147-1.652-.088-.394-.69-.78-1.086-1.744-.347-.84-4.76-7.29 1.224-6.333.22-.045-1.56-5.7-7.16-5.782S7.99 8.196 7.99 8.196" stroke-linejoin="bevel"/></g><g class="C"><path d="M11.247 15.768c-.78.872-.55 1.025-2.11 1.346-1.578.325-.65.904-.046 1.056.734.184 2.432.444 3.58-1.163.35-.49-.002-1.27-.482-1.468-.232-.096-.542-.216-.94.23z"/><path d="M11.196 15.753c-.08-.513.168-1.122.433-1.836.398-1.07 1.316-2.14.582-5.537-.547-2.53-4.22-.527-4.22-.184s.166 1.74-.06 3.365c-.297 2.122 1.35 3.916 3.246 3.733" class="B"/></g></g><g fill="#fff" class="D"><path d="M10.322 8.145c-.017.117.215.43.516.472s.558-.202.575-.32-.215-.246-.516-.288-.56.02-.575.136z" stroke-width=".239"/><path d="M19.486 7.906c.016.117-.215.43-.516.472s-.56-.202-.575-.32.215-.246.516-.288.56.02.575.136z" stroke-width=".119"/></g><path d="M20.562 7.095c.05.92-.198 1.545-.23 2.524-.046 1.422.678 3.05-.413 4.68" class="B C E"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#75aadb" d="M71.4 38.8c-1.5-.6-3.9-1-6.9-1.1-4.2-.1-9 .4-9.2.5v20c13.3.6 15.5-1.7 15.5-1.7 11.6-5.9 4.3-16.2.6-17.7z"/><path fill="#75aadb" d="M64 0C28.6 0 0 28.6 0 64s28.6 64 64 64 64-28.6 64-64S99.3 0 64 0zm28.6 89.8H82L64.4 63.5h-9V84h9v5.8H41.5v-5.7l7.6-.1-.1-45.9c-.8-.2-7.5-.8-7.5-.8V32c1 1 7.9 1.2 7.9 1.2 1.6.1 3.9.2 5.2-.1 9.3-1.7 16.4-.4 16.4-.4 14 3.2 14.2 15.8 10.3 22.6-3.5 5.8-10.3 7.2-10.3 7.2l14.4 21.8 7.2-.1v5.6z"/><path d="M41.595 87.073v-2.726l1.82-.141a59.125 59.125 0 013.752-.144h1.931V37.996l-.938-.127c-.516-.07-2.204-.248-3.752-.397l-2.813-.27v-2.51c0-2.332.027-2.495.39-2.3 1.583.847 10.7 1.07 15.83.388 4.202-.558 11.495-.425 14.035.257 5.483 1.472 9.11 4.646 10.824 9.473.717 2.018.817 5.847.216 8.224-.903 3.572-2.39 6.048-4.865 8.101-1.482 1.23-4.847 3.03-6.145 3.29-.397.079-.772.224-.832.321-.06.098 3.123 5.072 7.075 11.054l7.184 10.876 3.633-.068 3.634-.068V89.8l-5.242-.008-5.24-.007-8.82-13.234-8.817-13.234h-9.178V84.061h9.049V89.8H41.595zm25.158-29.162c3.476-.55 7.265-2.774 8.973-5.263 2.511-3.663 1.537-8.99-2.294-12.547-1.357-1.26-2.205-1.63-4.794-2.1-2.124-.386-8.66-.454-11.706-.122l-1.544.168-.058 10.083-.057 10.082.72.106c1.366.2 8.67-.075 10.76-.407z" fill="#fff" stroke="#fff" stroke-width=".788"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+168
View File
@@ -0,0 +1,168 @@
# AGENTS.md
This file provides guidance to AI coding assistants when working with code in this repository.
## Project Overview
The Coder Registry is a community-driven repository for Terraform modules and templates that extend Coder workspaces. It's organized with:
- **Modules**: Individual components and tools (IDEs, auth integrations, dev tools)
- **Templates**: Complete workspace configurations for different platforms
- **Namespaces**: Each contributor has their own namespace under `/registry/[namespace]/`
## Common Development Commands
### Formatting
```bash
bun run fmt # Format all code (Prettier + Terraform)
bun run fmt:ci # Check formatting (CI mode)
```
### Testing
```bash
# Test all modules with .tftest.hcl files
bun run test
# Test specific module (from module directory)
terraform init -upgrade
terraform test -verbose
# Validate Terraform syntax
./scripts/terraform_validate.sh
```
### Module Creation
```bash
# Generate new module scaffold
./scripts/new_module.sh namespace/module-name
```
### TypeScript Testing & Setup
The repository uses Bun for TypeScript testing with utilities:
- `test/test.ts` - Testing utilities for container management and Terraform operations
- `setup.ts` - Test cleanup (removes .tfstate files and test containers)
- Container-based testing with Docker for module validation
## Architecture & Organization
### Directory Structure
```
registry/[namespace]/
├── README.md # Contributor info with frontmatter
├── .images/ # Namespace avatar (avatar.png/svg)
├── modules/ # Individual components
│ └── [module]/ # Each module has main.tf, README.md, tests
└── templates/ # Complete workspace configs
└── [template]/ # Each template has main.tf, README.md
```
### Key Components
**Module Structure**: Each module contains:
- `main.tf` - Terraform implementation
- `README.md` - Documentation with YAML frontmatter
- `.tftest.hcl` - Terraform test files (required)
- `run.sh` - Optional startup script
**Template Structure**: Each template contains:
- `main.tf` - Complete Coder template configuration
- `README.md` - Documentation with YAML frontmatter
- Additional configs, scripts as needed
### README Frontmatter Requirements
All modules/templates require YAML frontmatter:
```yaml
---
display_name: "Module Name"
description: "Brief description"
icon: "../../../../.icons/tool.svg"
verified: false
tags: ["tag1", "tag2"]
---
```
## Testing Requirements
### Module Testing
- Every module MUST have `.tftest.hcl` test files
- Optional `main.test.ts` files for container-based testing or complex business logic validation
- Tests use Docker containers with `--network=host` flag
- Linux required for testing (Docker Desktop on macOS/Windows won't work)
- Use Colima or OrbStack on macOS instead of Docker Desktop
### Test Utilities
The `test/test.ts` file provides:
- `runTerraformApply()` - Execute Terraform with variables
- `executeScriptInContainer()` - Run coder_script resources in containers
- `testRequiredVariables()` - Validate required variables
- Container management functions
## Validation & Quality
### Automated Validation
The Go validation tool (`cmd/readmevalidation/`) checks:
- Repository structure integrity
- Contributor README files
- Module and template documentation
- Frontmatter format compliance
### Versioning
Use semantic versioning for modules:
- **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
## Dependencies & Tools
### Required Tools
- **Terraform** - Module development and testing
- **Docker** - Container-based testing
- **Bun** - JavaScript runtime for formatting/scripts
- **Go 1.23+** - Validation tooling
### Development Dependencies
- Prettier with Terraform and shell plugins
- TypeScript for test utilities
- Various npm packages for documentation processing
## Workflow Notes
### Contributing Process
1. Create namespace (first-time contributors)
2. Generate module/template files using scripts
3. Implement functionality and tests
4. Run formatting and validation
5. Submit PR with appropriate template
### Testing Workflow
- All modules must pass `terraform test`
- Use `bun run test` for comprehensive testing
- Format code with `bun run fmt` before submission
- Manual testing recommended for templates
### Namespace Management
- Each contributor gets unique namespace
- Namespace avatar required (avatar.png/svg in .images/)
- Namespace README with contributor info and frontmatter
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+3
View File
@@ -94,6 +94,9 @@ func validateCoderModuleReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderModuleReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
+76
View File
@@ -1,6 +1,7 @@
package main
import (
"bufio"
"errors"
"net/url"
"os"
@@ -16,11 +17,16 @@ import (
var (
supportedResourceTypes = []string{"modules", "templates"}
operatingSystems = []string{"windows", "macos", "linux"}
gfmAlertTypes = []string{"NOTE", "IMPORTANT", "CAUTION", "WARNING", "TIP"}
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
// Matches the format "> [!INFO]". Deliberately using a broad pattern to catch formatting issues that can mess up
// the renderer for the Registry website
gfmAlertRegex = regexp.MustCompile(`^>(\s*)\[!(\w+)\](\s*)(.*)`)
)
type coderResourceFrontmatter struct {
@@ -277,3 +283,73 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
}
return allReadmeFiles, nil
}
func validateResourceGfmAlerts(readmeBody string) []error {
trimmed := strings.TrimSpace(readmeBody)
if trimmed == "" {
return nil
}
var errs []error
var sourceLine string
isInsideGfmQuotes := false
isInsideCodeBlock := false
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
for lineScanner.Scan() {
sourceLine = lineScanner.Text()
if strings.HasPrefix(sourceLine, "```") {
isInsideCodeBlock = !isInsideCodeBlock
continue
}
if isInsideCodeBlock {
continue
}
isInsideGfmQuotes = isInsideGfmQuotes && strings.HasPrefix(sourceLine, "> ")
currentMatch := gfmAlertRegex.FindStringSubmatch(sourceLine)
if currentMatch == nil {
continue
}
// Nested GFM alerts is such a weird mistake that it's probably not really safe to keep trying to process the
// rest of the content, so this will prevent any other validations from happening for the given line
if isInsideGfmQuotes {
errs = append(errs, errors.New("registry does not support nested GFM alerts"))
continue
}
leadingWhitespace := currentMatch[1]
if len(leadingWhitespace) != 1 {
errs = append(errs, errors.New("GFM alerts must have one space between the '>' and the start of the GFM brackets"))
}
isInsideGfmQuotes = true
alertHeader := currentMatch[2]
upperHeader := strings.ToUpper(alertHeader)
if !slices.Contains(gfmAlertTypes, upperHeader) {
errs = append(errs, xerrors.Errorf("GFM alert type %q is not supported", alertHeader))
}
if alertHeader != upperHeader {
errs = append(errs, xerrors.Errorf("GFM alerts must be in all caps"))
}
trailingWhitespace := currentMatch[3]
if trailingWhitespace != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any trailing whitespace after the closing bracket"))
}
extraContent := currentMatch[4]
if extraContent != "" {
errs = append(errs, xerrors.Errorf("GFM alerts must not have any extra content on the same line"))
}
}
if gfmAlertRegex.Match([]byte(sourceLine)) {
errs = append(errs, xerrors.Errorf("README has an incomplete GFM alert at the end of the file"))
}
return errs
}
+3
View File
@@ -70,6 +70,9 @@ func validateCoderTemplateReadme(rm coderResourceReadme) []error {
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
for _, err := range validateResourceGfmAlerts(rm.body) {
errs = append(errs, addFilePathToError(rm.filePath, err))
}
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
errs = append(errs, fmErrs...)
}
+33
View File
@@ -0,0 +1,33 @@
---
display_name: NAMESPACE_NAME
bio: Brief description of what this namespace provides
github: your-github-username
avatar: ./.images/avatar.svg
linkedin: https://www.linkedin.com/in/your-profile
website: https://your-website.com
status: community
---
# NAMESPACE_NAME
Brief description of what this namespace provides. Include information about:
- What types of templates/modules you offer
- Your focus areas (e.g., specific cloud providers, technologies)
- Any special features or configurations
## Templates
List your available templates here:
- **template-name**: Brief description
## Modules
List your available modules here:
- **module-name**: Brief description
## Contributing
If you'd like to contribute to this namespace, please [open an issue](https://github.com/coder/registry/issues) or submit a pull request.
+58
View File
@@ -0,0 +1,58 @@
---
name: TEMPLATE_NAME
description: A brief description of what this template does
tags: [tag1, tag2, tag3]
icon: /icon/TEMPLATE_NAME.svg
---
# TEMPLATE_NAME
A brief description of what this template provides and its use case.
## Features
- Feature 1
- Feature 2
- Feature 3
## Requirements
- List any prerequisites or requirements
- Provider-specific requirements (e.g., Docker, AWS credentials)
- Minimum Coder version if applicable
## Usage
1. Step-by-step instructions on how to use this template
2. Any configuration that needs to be done
3. How to customize the template
## Variables
| Name | Description | Type | Default | Required |
| ----------- | --------------------------- | -------- | ----------------- | -------- |
| example_var | Description of the variable | `string` | `"default_value"` | no |
## Resources Created
- List of resources that will be created
- Brief description of each resource
## Customization
Explain how users can customize this template for their needs:
- How to modify the startup script
- How to add additional software
- How to configure different providers
## Troubleshooting
### Common Issues
- Issue 1 and its solution
- Issue 2 and its solution
## Contributing
Contributions are welcome! Please see the [contributing guidelines](../../CONTRIBUTING.md) for more information.
+172
View File
@@ -0,0 +1,172 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
# Add your provider here (e.g., docker, aws, gcp, azure)
# docker = {
# source = "kreuzwerker/docker"
# }
}
}
locals {
username = data.coder_workspace_owner.me.name
}
# Add your variables here
# variable "example_var" {
# default = "default_value"
# description = "Description of the variable"
# type = string
# }
# Configure your provider here
# provider "docker" {
# 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 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 templates, you can remove the "display_apps" block.
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
}
display_apps {
vscode = true
vscode_insiders = false
ssh_helper = false
port_forwarding_helper = true
web_terminal = true
}
}
# Add your resources here (e.g., docker container, VM, etc.)
# resource "docker_image" "main" {
# name = "codercom/enterprise-base:ubuntu"
# }
# resource "docker_container" "workspace" {
# count = data.coder_workspace.me.start_count
# image = docker_image.main.image_id
# # Uses lower() to avoid Docker restriction on container names.
# name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# # Hostname makes the shell more user friendly: coder@my-workspace:~$
# hostname = data.coder_workspace.me.name
# # Use the docker gateway if the access URL is 127.0.0.1
# entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\.0\.0\.1/", "host.docker.internal")]
# env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
# host {
# host = "host.docker.internal"
# ip = "host-gateway"
# }
# volumes {
# container_path = "/home/${local.username}"
# volume_name = docker_volume.home_volume[0].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 "docker_volume" "home_volume" {
# count = data.coder_workspace.me.start_count
# name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-home"
# # Protect the volume from being deleted due to changes in attributes.
# lifecycle {
# ignore_changes = all
# }
# # Add labels in Docker to keep track of orphan resources.
# labels {
# label = "coder.owner"
# value = data.coder_workspace_owner.me.name
# }
# labels {
# label = "coder.owner_id"
# value = data.coder_workspace_owner.me.id
# }
# labels {
# label = "coder.workspace_id"
# value = data.coder_workspace.me.id
# }
# labels {
# label = "coder.workspace_name"
# value = data.coder_workspace.me.name
# }
# }
resource "coder_metadata" "workspace_info" {
resource_id = coder_agent.main.id
item {
key = "TEMPLATE_NAME"
value = "TEMPLATE_NAME"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

+7
View File
@@ -0,0 +1,7 @@
---
display_name: Jash
bio: Coder user and contributor.
github: AJ0070
avatar: ./.images/avatar.png
status: community
---
+23
View File
@@ -0,0 +1,23 @@
---
display_name: "pgAdmin"
description: "A web-based interface for managing PostgreSQL databases in your Coder workspace."
icon: "../../../../.icons/pgadmin.svg"
maintainer_github: "AJ0070"
verified: false
tags: ["database", "postgres", "pgadmin", "web-ide"]
---
# pgAdmin
This module adds a pgAdmin app to your Coder workspace, providing a powerful web-based interface for managing PostgreSQL databases.
It can be served on a Coder subdomain for easy access, or on `localhost` if you prefer to use port-forwarding.
```tf
module "pgadmin" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/AJ0070/pgadmin/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,10 @@
import { describe } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "~test";
describe("pgadmin", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
});
+108
View File
@@ -0,0 +1,108 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
variable "agent_id" {
type = string
description = "The agent to install pgAdmin on."
}
variable "port" {
type = number
description = "The port to run pgAdmin on."
default = 5050
}
variable "subdomain" {
type = bool
description = "If true, the app will be served on a subdomain."
default = true
}
variable "config" {
type = any
description = "A map of pgAdmin configuration settings."
default = {
DEFAULT_EMAIL = "admin@coder.com"
DEFAULT_PASSWORD = "coderPASSWORD"
SERVER_MODE = false
MASTER_PASSWORD_REQUIRED = false
LISTEN_ADDRESS = "127.0.0.1"
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_app" "pgadmin" {
count = data.coder_workspace.me.start_count
agent_id = var.agent_id
display_name = "pgAdmin"
slug = "pgadmin"
icon = "/icon/pgadmin.svg"
url = local.url
subdomain = var.subdomain
share = "owner"
healthcheck {
url = local.healthcheck_url
interval = 5
threshold = 6
}
}
resource "coder_script" "pgadmin" {
agent_id = var.agent_id
display_name = "Install and run pgAdmin"
icon = "/icon/pgadmin.svg"
run_on_start = true
script = templatefile("${path.module}/run.sh", {
PORT = var.port,
LOG_PATH = "/tmp/pgadmin.log",
SERVER_BASE_PATH = local.server_base_path,
CONFIG = local.config_content,
PGADMIN_DATA_DIR = local.pgadmin_data_dir,
PGADMIN_LOG_DIR = local.pgadmin_log_dir,
PGADMIN_VENV_DIR = local.pgadmin_venv_dir
})
}
locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, "pgadmin")
url = "http://localhost:${var.port}${local.server_base_path}"
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/"
# pgAdmin data directories (user-local paths)
pgadmin_data_dir = "$HOME/.pgadmin"
pgadmin_log_dir = "$HOME/.pgadmin/logs"
pgadmin_venv_dir = "$HOME/.pgadmin/venv"
base_config = merge(var.config, {
LISTEN_PORT = var.port
# Override paths for user installation
DATA_DIR = local.pgadmin_data_dir
LOG_FILE = "${local.pgadmin_log_dir}/pgadmin4.log"
SQLITE_PATH = "${local.pgadmin_data_dir}/pgadmin4.db"
SESSION_DB_PATH = "${local.pgadmin_data_dir}/sessions"
STORAGE_DIR = "${local.pgadmin_data_dir}/storage"
# Disable initial setup prompts for automated deployment
SETUP_AUTH = false
})
config_with_path = var.subdomain ? local.base_config : merge(local.base_config, {
APPLICATION_ROOT = local.server_base_path
})
config_content = join("\n", [
for key, value in local.config_with_path :
format("%s = %s", key,
can(regex("^(true|false)$", tostring(value))) ? (value ? "True" : "False") :
can(tonumber(value)) ? tostring(value) :
format("'%s'", tostring(value))
)
])
}
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
PORT=${PORT}
LOG_PATH=${LOG_PATH}
SERVER_BASE_PATH=${SERVER_BASE_PATH}
BOLD='\033[0;1m'
printf "$${BOLD}Installing pgAdmin!\n"
# Check if Python 3 is available
if ! command -v python3 > /dev/null 2>&1; then
echo "⚠️ Warning: Python 3 is not installed. Please install Python 3 before using this module."
exit 0
fi
# Setup pgAdmin directories (from Terraform configuration)
PGADMIN_DATA_DIR="${PGADMIN_DATA_DIR}"
PGADMIN_LOG_DIR="${PGADMIN_LOG_DIR}"
PGADMIN_VENV_DIR="${PGADMIN_VENV_DIR}"
printf "Setting up pgAdmin directories...\n"
mkdir -p "$PGADMIN_DATA_DIR"
mkdir -p "$PGADMIN_LOG_DIR"
# Check if pgAdmin virtual environment already exists and is working
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ] && [ -f "$PGADMIN_VENV_DIR/bin/activate" ]; then
printf "🥳 pgAdmin virtual environment already exists\n\n"
else
printf "Creating Python virtual environment for pgAdmin...\n"
if ! python3 -m venv "$PGADMIN_VENV_DIR"; then
echo "⚠️ Warning: Failed to create virtual environment"
exit 0
fi
printf "Installing pgAdmin 4 in virtual environment...\n"
if ! "$PGADMIN_VENV_DIR/bin/pip" install pgadmin4; then
echo "⚠️ Warning: Failed to install pgAdmin4"
exit 0
fi
printf "🥳 pgAdmin has been installed successfully\n\n"
fi
printf "$${BOLD}Configuring pgAdmin...\n"
if [ -f "$PGADMIN_VENV_DIR/bin/pgadmin4" ]; then
# pgAdmin installs to a predictable location in the virtual environment
PYTHON_VERSION=$("$PGADMIN_VENV_DIR/bin/python" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
PGADMIN_INSTALL_DIR="$PGADMIN_VENV_DIR/lib/python$PYTHON_VERSION/site-packages/pgadmin4"
# Create pgAdmin config file in the correct location (next to config.py)
cat > "$PGADMIN_INSTALL_DIR/config_local.py" << EOF
# pgAdmin configuration for Coder workspace
${CONFIG}
EOF
printf "📄 Config written to $PGADMIN_INSTALL_DIR/config_local.py\n"
printf "$${BOLD}Starting pgAdmin in background...\n"
printf "📝 Check logs at $${LOG_PATH}\n"
printf "🌐 Serving at http://localhost:${PORT}${SERVER_BASE_PATH}\n"
# Create required directories
mkdir -p "$PGADMIN_DATA_DIR/sessions"
mkdir -p "$PGADMIN_DATA_DIR/storage"
# Start pgadmin4 from the virtual environment with proper environment
cd "$PGADMIN_DATA_DIR"
PYTHONPATH="$PGADMIN_INSTALL_DIR:$${PYTHONPATH:-}" "$PGADMIN_VENV_DIR/bin/pgadmin4" > "$${LOG_PATH}" 2>&1 &
else
printf "⚠️ Warning: pgAdmin4 virtual environment not found\n"
printf "📝 Installation may have failed - check logs above\n"
fi
@@ -2,7 +2,7 @@
display_name: Amp CLI
icon: ../../../../.icons/sourcegraph-amp.svg
description: Sourcegraph's AI coding agent with deep codebase understanding and intelligent code search capabilities
verified: false
verified: true
tags: [agent, sourcegraph, amp, ai, tasks]
---
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
```tf
module "amp-cli" {
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.0.3"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key
install_sourcegraph_amp = true
@@ -60,7 +60,7 @@ variable "sourcegraph_amp_api_key" {
module "amp-cli" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
version = "1.0.2"
version = "1.0.3"
agent_id = coder_agent.example.id
sourcegraph_amp_api_key = var.sourcegraph_amp_api_key # recommended for authenticated usage
install_sourcegraph_amp = true
@@ -0,0 +1,16 @@
---
display_name: Externally Managed Workspace
description: A template to provision externally managed resources as Coder workspaces
icon: ../../../../.icons/electric-plug-emoji.svg
verified: true
tags: [external]
---
# Externally Managed Workspace Template
> [!IMPORTANT]
> External agents require a [Premium](https://coder.com/pricing) Coder license.
This template provides a minimal scaffolding for creating Coder workspaces that connect to externally provisioned compute resources.
Use this template as a starting point to build your own custom templates for scenarios where you need to connect to existing infrastructure.
@@ -0,0 +1,74 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.10"
}
}
}
data "coder_parameter" "agent_config" {
name = "agent_config"
display_name = "Agent Configuration"
description = "Select the operating system and architecture combination for the agent"
type = "string"
default = "linux-amd64"
option {
name = "Linux AMD64"
value = "linux-amd64"
}
option {
name = "Linux ARM64"
value = "linux-arm64"
}
option {
name = "Linux ARMv7"
value = "linux-armv7"
}
option {
name = "Windows AMD64"
value = "windows-amd64"
}
option {
name = "Windows ARM64"
value = "windows-arm64"
}
option {
name = "macOS AMD64"
value = "darwin-amd64"
}
option {
name = "macOS ARM64 (Apple Silicon)"
value = "darwin-arm64"
}
}
data "coder_workspace" "me" {}
locals {
agent_config = split("-", data.coder_parameter.agent_config.value)
agent_os = local.agent_config[0]
agent_arch = local.agent_config[1]
}
resource "coder_agent" "main" {
arch = local.agent_arch
os = local.agent_os
}
resource "coder_external_agent" "main" {
agent_id = coder_agent.main.id
}
# Adds code-server
# See all available modules at https://registry.coder.com/modules
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
}
@@ -24,6 +24,7 @@ module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "2.0.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder/projects"
install_claude_code = true
claude_code_version = "latest"
@@ -44,9 +45,10 @@ variable "anthropic_api_key" {
sensitive = true
}
resource "coder_env" "anthropic_api_key" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_API_KEY"
value = var.anthropic_api_key
}
# We are using presets to set the prompts, image, and set up instructions
@@ -174,19 +176,22 @@ data "coder_parameter" "preview_port" {
# Other variables for Claude Code
resource "coder_env" "claude_task_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
value = data.coder_parameter.ai_prompt.value
}
resource "coder_env" "app_status_slug" {
agent_id = coder_agent.main.id
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_APP_STATUS_SLUG"
value = "ccw"
}
resource "coder_env" "claude_system_prompt" {
agent_id = coder_agent.main.id
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
agent_id = coder_agent.main.id
agent_name = "main"
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
value = data.coder_parameter.system_prompt.value
}
data "coder_provisioner" "me" {}
@@ -296,48 +301,42 @@ module "code-server" {
# 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
agent_id = coder_agent.main.id
agent_name = "main"
order = 1
}
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
agent_name = "main"
}
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
agent_id = coder_agent.main.id
agent_name = "main"
}
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
}
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder/projects"
# 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"
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
}
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder/projects"
}
resource "docker_volume" "home_volume" {
@@ -369,6 +368,7 @@ resource "docker_volume" "home_volume" {
resource "coder_app" "preview" {
agent_id = coder_agent.main.id
agent_name = "main"
slug = "preview"
display_name = "Preview your app"
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
@@ -422,4 +422,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

@@ -28,6 +28,7 @@ module "agentapi" {
cli_app_display_name = "Goose CLI"
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
enable_tasks = var.enable_tasks # Set to false to disable AI tasks
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = local.start_script
+8 -1
View File
@@ -156,6 +156,11 @@ variable "module_dir_name" {
description = "Name of the subdirectory in the home directory for module files."
}
variable "enable_tasks" {
type = bool
description = "Whether to enable AI tasks. When false, the coder_ai_task resource will not be created, avoiding the requirement for an 'AI Prompt' parameter."
default = true
}
locals {
# we always trim the slash for consistency
@@ -240,7 +245,9 @@ resource "coder_app" "agentapi_cli" {
}
resource "coder_ai_task" "agentapi" {
count = var.enable_tasks ? 1 : 0
sidebar_app {
id = coder_app.agentapi_web.id
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ Automatically logs the user into Coder when creating their workspace.
module "coder-login" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/coder-login/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
}
```
+8 -9
View File
@@ -17,15 +17,14 @@ variable "agent_id" {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
resource "coder_script" "coder-login" {
resource "coder_env" "coder_session_token" {
agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", {
CODER_USER_TOKEN : data.coder_workspace_owner.me.session_token,
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
})
display_name = "Coder Login"
icon = "/icon/coder.svg"
run_on_start = true
start_blocks_login = true
name = "CODER_SESSION_TOKEN"
value = data.coder_workspace_owner.me.session_token
}
resource "coder_env" "coder_url" {
agent_id = var.agent_id
name = "CODER_URL"
value = data.coder_workspace.me.access_url
}
@@ -0,0 +1,65 @@
# Test for coder-login module
run "test_coder_login_module" {
command = plan
variables {
agent_id = "test-agent-id"
}
# Test that the coder_env resources are created with correct configuration
assert {
condition = coder_env.coder_session_token.agent_id == "test-agent-id"
error_message = "CODER_SESSION_TOKEN agent ID should match the input variable"
}
assert {
condition = coder_env.coder_session_token.name == "CODER_SESSION_TOKEN"
error_message = "Environment variable name should be 'CODER_SESSION_TOKEN'"
}
assert {
condition = coder_env.coder_url.agent_id == "test-agent-id"
error_message = "CODER_URL agent ID should match the input variable"
}
assert {
condition = coder_env.coder_url.name == "CODER_URL"
error_message = "Environment variable name should be 'CODER_URL'"
}
}
# Test with mock data sources
run "test_with_mock_data" {
command = plan
variables {
agent_id = "mock-agent"
}
# Mock the data sources for testing
override_data {
target = data.coder_workspace.me
values = {
access_url = "https://coder.example.com"
}
}
override_data {
target = data.coder_workspace_owner.me
values = {
session_token = "mock-session-token"
}
}
# Verify environment variables get the mocked values
assert {
condition = coder_env.coder_url.value == "https://coder.example.com"
error_message = "CODER_URL should match workspace access_url"
}
assert {
condition = coder_env.coder_session_token.value == "mock-session-token"
error_message = "CODER_SESSION_TOKEN should match workspace owner session_token"
}
}
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/env sh
# Automatically authenticate the user if they are not
# logged in to another deployment
BOLD='\033[0;1m'
printf "$${BOLD}Logging into Coder...\n\n$${RESET}"
if ! coder list > /dev/null 2>&1; then
set +x
coder login --token="${CODER_USER_TOKEN}" --url="${CODER_DEPLOYMENT_URL}"
else
echo "You are already authenticated with coder."
fi
+3 -3
View File
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.1"
version = "1.3.2"
agent_id = coder_agent.example.id
}
```
@@ -29,7 +29,7 @@ module "cursor" {
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.1"
version = "1.3.2"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.3.1"
version = "1.3.2"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
mcp = jsonencode({
+1
View File
@@ -98,6 +98,7 @@ resource "coder_script" "cursor_mcp" {
set -eu
mkdir -p "$HOME/.cursor"
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.cursor/mcp.json"
chmod 600 "$HOME/.cursor/mcp.json"
EOT
}
+4 -3
View File
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -26,6 +26,7 @@ module "jfrog" {
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
conda = ["conda", "conda-local"]
}
}
```
@@ -45,7 +46,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "email"
@@ -74,7 +75,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
module "jfrog" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jfrog-oauth/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://example.jfrog.io"
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
@@ -0,0 +1,6 @@
channels:
%{ for REPO in REPOS ~}
- https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/conda/${REPO}
%{ endfor ~}
- defaults
ssl_verify: true
@@ -126,4 +126,28 @@ EOF`;
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:@${fakeFrogApi}/conda/conda-main
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
- https://${user}:@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
});
@@ -58,6 +58,7 @@ variable "package_managers" {
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
conda = optional(list(string), [])
})
description = <<-EOF
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
@@ -67,6 +68,7 @@ variable "package_managers" {
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
}
EOF
}
@@ -98,6 +100,9 @@ locals {
pip_conf = templatefile(
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
)
conda_conf = templatefile(
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
)
}
data "coder_workspace" "me" {}
@@ -125,6 +130,9 @@ resource "coder_script" "jfrog" {
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
CONDA_CONF = local.conda_conf
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
}
))
run_on_start = true
+13
View File
@@ -81,6 +81,19 @@ else
fi
fi
# Configure conda to use the Artifactory "conda" repository.
if [ -z "${HAS_CONDA}" ]; then
not_configured conda
else
echo "🐍 Configuring conda..."
# Create conda config directory if it doesn't exist
mkdir -p ~/.conda
cat << EOF > ~/.condarc
${CONDA_CONF}
EOF
config_complete
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
+12 -8
View File
@@ -13,7 +13,7 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -22,6 +22,7 @@ module "jfrog" {
go = ["go", "another-go-repo"]
pypi = ["pypi", "extra-index-pypi"]
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
conda = ["conda", "conda-local"]
}
}
```
@@ -40,30 +41,33 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://YYYY.jfrog.io"
artifactory_access_token = var.artifactory_access_token # An admin access token
package_managers = {
npm = ["npm-local"]
go = ["go-local"]
pypi = ["pypi-local"]
npm = ["npm-local"]
go = ["go-local"]
pypi = ["pypi-local"]
conda = ["conda-local"]
}
}
```
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip` commands.
You should now be able to install packages from Artifactory using both the `jf npm`, `jf go`, `jf pip` and `npm`, `go`, `pip`, `conda` commands.
```shell
jf npm install prettier
jf go get github.com/golang/example/hello
jf pip install requests
conda install numpy
```
```shell
npm install prettier
go get github.com/golang/example/hello
pip install requests
conda install numpy
```
### Configure code-server with JFrog extension
@@ -73,7 +77,7 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
```tf
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -93,7 +97,7 @@ data "coder_workspace" "me" {}
module "jfrog" {
source = "registry.coder.com/coder/jfrog-token/coder"
version = "1.0.31"
version = "1.1.0"
agent_id = coder_agent.example.id
jfrog_url = "https://XXXX.jfrog.io"
artifactory_access_token = var.artifactory_access_token
@@ -0,0 +1,6 @@
channels:
%{ for REPO in REPOS ~}
- https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/conda/${REPO}
%{ endfor ~}
- defaults
ssl_verify: true
@@ -162,4 +162,29 @@ EOF`;
'if [ -z "YES" ]; then\n not_configured go',
);
});
it("generates a conda config with multiple repos", async () => {
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
agent_id: "some-agent-id",
jfrog_url: fakeFrogUrl,
artifactory_access_token: "XXXX",
package_managers: JSON.stringify({
conda: ["conda-main", "conda-secondary", "conda-local"],
}),
});
const coderScript = findResourceInstance(state, "coder_script");
const condaStanza = `cat << EOF > ~/.condarc
channels:
- https://${user}:${token}@${fakeFrogApi}/conda/conda-main
- https://${user}:${token}@${fakeFrogApi}/conda/conda-secondary
- https://${user}:${token}@${fakeFrogApi}/conda/conda-local
- defaults
ssl_verify: true
EOF`;
expect(coderScript.script).toContain(condaStanza);
expect(coderScript.script).toContain(
'if [ -z "YES" ]; then\n not_configured conda',
);
});
});
@@ -91,6 +91,7 @@ variable "package_managers" {
go = optional(list(string), [])
pypi = optional(list(string), [])
docker = optional(list(string), [])
conda = optional(list(string), [])
})
description = <<-EOF
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
@@ -100,6 +101,7 @@ variable "package_managers" {
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
conda = ["YOUR_CONDA_REPO_KEY", "ANOTHER_CONDA_REPO_KEY"]
}
EOF
}
@@ -131,6 +133,9 @@ locals {
pip_conf = templatefile(
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
)
conda_conf = templatefile(
"${path.module}/conda.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.conda })
)
}
# Configure the Artifactory provider
@@ -171,6 +176,9 @@ resource "coder_script" "jfrog" {
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
HAS_CONDA = length(var.package_managers.conda) == 0 ? "" : "YES"
CONDA_CONF = local.conda_conf
REPOSITORY_CONDA = try(element(var.package_managers.conda, 0), "")
}
))
run_on_start = true
+13
View File
@@ -80,6 +80,19 @@ else
fi
fi
# Configure conda to use the Artifactory "conda" repository.
if [ -z "${HAS_CONDA}" ]; then
not_configured conda
else
echo "🐍 Configuring conda..."
# Create conda config directory if it doesn't exist
mkdir -p ~/.conda
cat << EOF > ~/.condarc
${CONDA_CONF}
EOF
config_complete
fi
# Install the JFrog vscode extension for code-server.
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
while ! [ -x /tmp/code-server/bin/code-server ]; do
+27 -9
View File
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.0.0"
version = "1.1.0"
agent_id = coder_agent.example.id
}
```
@@ -31,21 +31,39 @@ module "kiro" {
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.0.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
### Open with custom display name and order
### Configure MCP servers for Kiro
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.kiro/settings/mcp.json` using a `coder_script` on workspace start.
The following example configures Kiro to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
```tf
module "kiro" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
display_name = "Kiro AI IDE"
order = 1
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kiro/coder"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
mcp = jsonencode({
mcpServers = {
"github" : {
"url" : "https://api.githubcopilot.com/mcp/",
"headers" : {
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
},
"type" : "http"
}
}
})
}
data "coder_external_auth" "github" {
id = "github"
}
```
+36
View File
@@ -3,6 +3,11 @@ import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
runContainer,
execContainer,
removeContainer,
findResourceInstance,
readFileContainer,
} from "~test";
describe("kiro", async () => {
@@ -90,4 +95,35 @@ describe("kiro", async () => {
expect(coder_app?.instances[0].attributes.group).toBe("AI IDEs");
});
it("writes ~/.kiro/settings/mcp.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {
const mcp = JSON.stringify({
servers: { demo: { url: "http://localhost:1234" } },
});
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
mcp,
});
const script = findResourceInstance(
state,
"coder_script",
"kiro_mcp",
).script;
const resp = await execContainer(id, ["sh", "-c", script]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
const content = await readFileContainer(
id,
"/root/.kiro/settings/mcp.json",
);
expect(content).toBe(mcp);
} finally {
await removeContainer(id);
}
});
});
+26
View File
@@ -50,9 +50,19 @@ variable "display_name" {
default = "Kiro IDE"
}
variable "mcp" {
type = string
description = "JSON-encoded string to configure MCP servers for Kiro. When set, writes ~/.kiro/settings/mcp.json."
default = ""
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
resource "coder_app" "kiro" {
agent_id = var.agent_id
external = true
@@ -75,6 +85,22 @@ resource "coder_app" "kiro" {
])
}
resource "coder_script" "kiro_mcp" {
count = var.mcp != "" ? 1 : 0
agent_id = var.agent_id
display_name = "Kiro MCP"
icon = "/icon/kiro.svg"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/bin/sh
set -eu
mkdir -p "$HOME/.kiro/settings"
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.kiro/settings/mcp.json"
chmod 600 "$HOME/.kiro/settings/mcp.json"
EOT
}
output "kiro_url" {
value = coder_app.kiro.url
description = "Kiro IDE URL."
@@ -0,0 +1,25 @@
---
display_name: RStudio Server
description: Deploy the Rocker Project distribution of RStudio Server in your Coder workspace.
icon: ../../../../.icons/rstudio.svg
verified: true
tags: [rstudio, ide, web]
---
# RStudio Server
> [!NOTE]
> This module requires `docker` to be available in the workspace. Check [Docker in Workspaces](https://coder.com/docs/admin/templates/extending-templates/docker-in-workspaces) to learn how you can set it up.
Deploy the Rocker Project distribution of RStudio Server in your Coder workspace.
![RStudio Server](../../.images/rstudio-server.png)
```tf
module "rstudio-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/rstudio-server/coder"
version = "0.9.0"
agent_id = coder_agent.example.id
}
```
@@ -0,0 +1,123 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "docker_socket" {
type = string
description = "(Optional) Docker socket URI"
default = ""
}
variable "rstudio_server_version" {
type = string
description = "RStudio Server version"
default = "4.5.1"
}
variable "disable_auth" {
type = bool
description = "Disable auth"
default = true
}
variable "rstudio_user" {
type = string
description = "RStudio user"
default = "rstudio"
sensitive = true
}
variable "rstudio_password" {
type = string
description = "RStudio password"
default = "rstudio"
sensitive = true
}
variable "project_path" {
type = string
description = "The path to RStudio project, it will be mounted in the container."
default = null
}
variable "port" {
type = number
description = "The port to run rstudio-server on."
default = 8787
}
variable "enable_renv" {
type = bool
description = "If renv.lock exists, renv will restore the environment and install dependencies"
default = true
}
variable "renv_cache_volume" {
type = string
description = "The name of the volume used by Renv to preserve dependencies between container restarts"
default = "renv-cache-volume"
}
variable "share" {
type = string
default = "owner"
validation {
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "rstudio-server" {
agent_id = var.agent_id
display_name = "rstudio-server"
icon = "/icon/rstudio.svg"
script = templatefile("${path.module}/run.sh", {
DOCKER_HOST : var.docker_socket,
SERVER_VERSION : var.rstudio_server_version,
DISABLE_AUTH : var.disable_auth,
RSTUDIO_USER : var.rstudio_user,
RSTUDIO_PASSWORD : var.rstudio_password,
PROJECT_PATH : var.project_path,
PORT : var.port,
ENABLE_RENV : var.enable_renv,
RENV_CACHE_VOLUME : var.renv_cache_volume,
})
run_on_start = true
}
resource "coder_app" "rstudio-server" {
agent_id = var.agent_id
slug = "rstudio-server"
display_name = "RStudio Server"
url = "http://localhost:${var.port}"
icon = "/icon/rstudio.svg"
subdomain = true
share = var.share
order = var.order
group = var.group
}
@@ -0,0 +1,57 @@
#!/usr/bin/env sh
set -eu
BOLD='\033[0;1m'
RESET='\033[0m'
printf "$${BOLD}Starting RStudio Server (Rocker)...$${RESET}\n"
# Wait for docker to become ready
max_attempts=10
delay=2
attempt=1
while ! docker ps; do
if [ $attempt -ge $max_attempts ]; then
echo "Failed to list containers after $${max_attempts} attempts."
exit 1
fi
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
sleep $delay
attempt=$(expr "$attempt" + 1)
delay=$(expr "$delay" \* 2) # exponential backoff
done
# Pull the specified version
IMAGE="rocker/rstudio:${SERVER_VERSION}"
docker pull "$${IMAGE}"
# Create (or reuse) a persistent renv cache volume
docker volume create "${RENV_CACHE_VOLUME}"
# Run container (auto-remove on stop)
docker run -d --rm \
--name rstudio-server \
-p "${PORT}:8787" \
-e DISABLE_AUTH="${DISABLE_AUTH}" \
-e USER="${RSTUDIO_USER}" \
-e PASSWORD="${RSTUDIO_PASSWORD}" \
-e RENV_PATHS_CACHE="/renv/cache" \
-v "${PROJECT_PATH}:/home/${RSTUDIO_USER}/project" \
-v "${RENV_CACHE_VOLUME}:/renv/cache" \
"$${IMAGE}"
# Make RENV_CACHE_VOLUME writable to USER
docker exec rstudio-server bash -c 'chmod -R 0777 /renv/cache'
# Optional renv restore
if [ "${ENABLE_RENV}" = "true" ] && [ -f "${PROJECT_PATH}/renv.lock" ]; then
echo "Restoring R environment via renv..."
docker exec -u "${RSTUDIO_USER}" rstudio-server R -q -e \
'if (!requireNamespace("renv", quietly = TRUE)) install.packages("renv", repos="https://cloud.r-project.org"); renv::restore(prompt = FALSE)'
fi
[ "${DISABLE_AUTH}" != "true" ] && echo "User: ${RSTUDIO_USER}"
printf "\n$${BOLD}RStudio Server ${SERVER_VERSION} is running on port ${PORT}$${RESET}\n"
+33 -2
View File
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
}
```
@@ -29,8 +29,39 @@ module "windsurf" {
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.1"
version = "1.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
```
### Configure MCP servers for Windsurf
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.codeium/windsurf/mcp_config.json` using a `coder_script` on workspace start.
The following example configures Windsurf to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
```tf
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
mcp = jsonencode({
mcpServers = {
"github" : {
"url" : "https://api.githubcopilot.com/mcp/",
"headers" : {
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
},
"type" : "http"
}
}
})
}
data "coder_external_auth" "github" {
id = "github"
}
```
@@ -3,6 +3,11 @@ import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
runContainer,
execContainer,
removeContainer,
findResourceInstance,
readFileContainer,
} from "~test";
describe("windsurf", async () => {
@@ -85,4 +90,35 @@ describe("windsurf", async () => {
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
it("writes ~/.codeium/windsurf/mcp_config.json when mcp provided", async () => {
const id = await runContainer("alpine");
try {
const mcp = JSON.stringify({
servers: { demo: { url: "http://localhost:1234" } },
});
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
mcp,
});
const script = findResourceInstance(
state,
"coder_script",
"windsurf_mcp",
).script;
const resp = await execContainer(id, ["sh", "-c", script]);
if (resp.exitCode !== 0) {
console.log(resp.stdout);
console.log(resp.stderr);
}
expect(resp.exitCode).toBe(0);
const content = await readFileContainer(
id,
"/root/.codeium/windsurf/mcp_config.json",
);
expect(content).toBe(mcp);
} finally {
await removeContainer(id);
}
});
});
+40 -2
View File
@@ -38,15 +38,37 @@ variable "group" {
default = null
}
variable "slug" {
type = string
description = "The slug of the app."
default = "windsurf"
}
variable "display_name" {
type = string
description = "The display name of the app."
default = "Windsurf Editor"
}
variable "mcp" {
type = string
description = "JSON-encoded string to configure MCP servers for Windsurf. When set, writes ~/.codeium/windsurf/mcp_config.json."
default = ""
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
}
resource "coder_app" "windsurf" {
agent_id = var.agent_id
external = true
icon = "/icon/windsurf.svg"
slug = "windsurf"
display_name = "Windsurf Editor"
slug = var.slug
display_name = var.display_name
order = var.order
group = var.group
url = join("", [
@@ -63,6 +85,22 @@ resource "coder_app" "windsurf" {
])
}
resource "coder_script" "windsurf_mcp" {
count = var.mcp != "" ? 1 : 0
agent_id = var.agent_id
display_name = "Windsurf MCP"
icon = "/icon/windsurf.svg"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/bin/sh
set -eu
mkdir -p "$HOME/.codeium/windsurf"
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.codeium/windsurf/mcp_config.json"
chmod 600 "$HOME/.codeium/windsurf/mcp_config.json"
EOT
}
output "windsurf_url" {
value = coder_app.windsurf.url
description = "Windsurf Editor URL."
@@ -326,6 +326,7 @@ 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
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
}
+12 -21
View File
@@ -201,28 +201,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.dev[0].id
agent_name = "dev"
folder = "/home/coder"
}
locals {
@@ -293,4 +284,4 @@ resource "coder_metadata" "workspace_info" {
resource "aws_ec2_instance_state" "dev" {
instance_id = aws_instance.dev.id
state = data.coder_workspace.me.transition == "start" ? "running" : "stopped"
}
}
@@ -35,7 +35,7 @@ This means, when the workspace restarts, any tools or files outside of the home
### Persistent VM
> [!IMPORTANT]
> [!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.
+12 -21
View File
@@ -144,28 +144,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
locals {
@@ -322,4 +313,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${data.coder_parameter.home_size.value} GiB"
}
}
}
@@ -35,7 +35,7 @@ This means, when the workspace restarts, any tools or files outside of the data
### Persistent VM
> [!IMPORTANT]
> [!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.
@@ -37,6 +37,7 @@ module "windows_rdp" {
admin_password = random_password.admin_password.result
agent_id = resource.coder_agent.main.id
agent_name = "main"
resource_id = null # Unused, to be removed in a future version
}
@@ -272,28 +272,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "digitalocean_volume" "home_volume" {
@@ -358,4 +349,4 @@ resource "coder_metadata" "volume-info" {
key = "size"
value = "${digitalocean_volume.home_volume.size} GiB"
}
}
}
@@ -330,28 +330,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/workspaces"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/workspaces"
}
resource "coder_metadata" "container_info" {
@@ -369,4 +360,4 @@ resource "coder_metadata" "container_info" {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
}
+12 -21
View File
@@ -129,28 +129,19 @@ module "code-server" {
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains-gateway
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
# JetBrains IDEs to make available for the user to select
jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
default = "IU"
# Default folder to open when starting a JetBrains IDE
folder = "/home/coder"
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
order = 2
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "docker_volume" "home_volume" {
@@ -217,4 +208,4 @@ resource "docker_container" "workspace" {
label = "coder.workspace_name"
value = data.coder_workspace.me.name
}
}
}
@@ -291,28 +291,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/workspaces"
}
# Create metadata for the workspace and home disk.
@@ -338,4 +329,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
}
+12 -21
View File
@@ -99,28 +99,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "google_compute_instance" "dev" {
@@ -181,4 +172,4 @@ resource "coder_metadata" "home_info" {
key = "size"
value = "${google_compute_disk.root.size} GiB"
}
}
}
@@ -52,28 +52,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
# See https://registry.terraform.io/modules/terraform-google-modules/container-vm
@@ -122,6 +113,7 @@ resource "google_compute_instance" "dev" {
resource "coder_agent_instance" "dev" {
count = data.coder_workspace.me.start_count
agent_id = coder_agent.main.id
agent_name = "main"
instance_id = google_compute_instance.dev[0].instance_id
}
@@ -133,4 +125,4 @@ resource "coder_metadata" "workspace_info" {
key = "image"
value = module.gce-container.container.image
}
}
}
@@ -422,28 +422,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "coder_metadata" "container_info" {
@@ -461,4 +452,4 @@ resource "coder_metadata" "container_info" {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
}
@@ -106,28 +106,19 @@ module "code-server" {
# 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
order = 1
}
# See https://registry.coder.com/modules/coder/jetbrains
module "jetbrains" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/modules/coder/jetbrains/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
agent_name = "main"
folder = "/home/coder"
}
resource "kubernetes_persistent_volume_claim" "home" {
@@ -319,4 +310,4 @@ resource "kubernetes_pod" "main" {
}
}
}
}
}
@@ -177,6 +177,7 @@ resource "coder_agent" "main" {
# code-server
resource "coder_app" "code-server" {
agent_id = coder_agent.main.id
agent_name = "main"
slug = "code-server"
display_name = "code-server"
icon = "/icon/code.svg"
@@ -118,8 +118,9 @@ module "code-server" {
# 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
agent_id = coder_agent.main.id
agent_name = "main"
order = 1
}
locals {
+7 -4
View File
@@ -34,9 +34,10 @@ resource "coder_agent" "main" {
# Use this to set environment variables in your workspace
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env
resource "coder_env" "welcome_message" {
agent_id = coder_agent.main.id
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
agent_id = coder_agent.main.id
agent_name = "main"
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
}
# Adds code-server
@@ -48,13 +49,15 @@ module "code-server" {
# 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_id = coder_agent.main.id
agent_name = "main"
}
# Runs a script at workspace start/stop or on a cron schedule
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
resource "coder_script" "startup_script" {
agent_id = coder_agent.main.id
agent_name = "main"
display_name = "Startup Script"
script = <<-EOF
#!/bin/sh
@@ -0,0 +1,87 @@
---
display_name: DigitalOcean Region
description: A parameter with human region names and icons
icon: ../../../../.icons/digital-ocean.svg
verified: true
tags: [helper, parameter, digitalocean, regions]
---
# DigitalOcean Region
This module adds DigitalOcean regions to your Coder template with automatic GPU filtering. You can customize display names and icons using the `custom_names` and `custom_icons` arguments.
The simplest usage is:
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
default = "ams3"
}
```
## Examples
### Basic usage
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
}
```
### With custom configuration
```tf
module "digitalocean-region" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/umair/digitalocean-region/coder"
version = "1.0.0"
default = "ams3"
mutable = true
custom_icons = {
"ams3" = "/emojis/1f1f3-1f1f1.png"
}
custom_names = {
"ams3" = "Europe - Amsterdam (Primary)"
}
}
```
### GPU-only toggle (internal parameter)
This module automatically exposes a "GPU-only regions" checkbox in the template UI. When checked, it shows only GPU-capable regions and auto-selects the first one. When unchecked, it shows all available regions.
## Available Regions
Refer to DigitalOceans official availability matrix for the most up-to-date information.
- GPU availability: currently only in `nyc2` and `tor1` (per DO docs). Others are non-GPU.
- See: https://docs.digitalocean.com/platform/regional-availability/
### All datacenters (GPU status)
- `nyc2` - New York, United States (Legacy) - **GPU available**
- `tor1` - Toronto, Canada - **GPU available**
- `nyc3` - New York, United States
- `ams3` - Amsterdam, Netherlands
- `sfo3` - San Francisco, United States
- `sgp1` - Singapore
- `lon1` - London, United Kingdom
- `fra1` - Frankfurt, Germany
- `blr1` - Bangalore, India
- `syd1` - Sydney, Australia
- `atl1` - Atlanta, United States
- `nyc1` - New York, United States (Legacy)
- `sfo2` - San Francisco, United States (Legacy)
- `sfo1` - San Francisco, United States (Legacy)
- `ams2` - Amsterdam, Netherlands (Legacy)
## Associated template
Also see the Coder template registry for a [DigitalOcean Droplet template](https://registry.coder.com/templates/digitalocean-droplet) that provisions workspaces as DigitalOcean Droplets.
@@ -0,0 +1,45 @@
import { describe, expect, it } from "bun:test";
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("digitalocean-region", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {});
it("default output", async () => {
const state = await runTerraformApply(import.meta.dir, {});
expect(state.outputs.value.value).toBe("ams2");
});
it("customized default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["nyc1","ams3"]',
default: "ams3",
});
expect(state.outputs.value.value).toBe("ams3");
});
it("gpu only invalid default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["nyc1"]',
default: "nyc1",
gpu_only: "true",
});
expect(state.outputs.value.value).toBe("nyc1");
});
it("gpu only valid default", async () => {
const state = await runTerraformApply(import.meta.dir, {
regions: '["tor1"]',
default: "tor1",
gpu_only: "true",
});
expect(state.outputs.value.value).toBe("tor1");
});
// Add more tests as needed for coder_parameter_order or other features
});
@@ -0,0 +1,187 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.11"
}
}
}
variable "display_name" {
default = "DigitalOcean Region"
description = "The display name of the parameter."
type = string
}
variable "description" {
default = "The region to deploy workspace infrastructure."
description = "The description of the parameter."
type = string
}
variable "default" {
default = null
description = "Default region"
type = string
}
variable "mutable" {
default = false
description = "Whether the parameter can be changed after creation."
type = bool
}
variable "custom_names" {
default = {}
description = "A map of custom display names for region IDs."
type = map(string)
}
variable "custom_icons" {
default = {}
description = "A map of custom icons for region IDs."
type = map(string)
}
variable "single_zone_per_region" {
default = true
description = "Whether to only include a single zone per region."
type = bool
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
default = null
}
data "coder_parameter" "gpu_only" {
name = "digitalocean_gpu_only"
display_name = "GPU-only regions"
description = "Show only regions with GPUs"
type = "bool"
form_type = "checkbox"
default = false
mutable = var.mutable
order = var.coder_parameter_order
}
locals {
zones = {
# Active datacenters (recommended for new workloads)
"nyc1" = {
gpu = false
name = "New York City, USA (NYC1)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"nyc3" = {
gpu = false
name = "New York City, USA (NYC3)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"ams3" = {
gpu = false
name = "Amsterdam, Netherlands"
icon = "/emojis/1f1f3-1f1f1.png"
}
"sfo3" = {
gpu = false
name = "San Francisco, USA"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sgp1" = {
gpu = false
name = "Singapore"
icon = "/emojis/1f1f8-1f1ec.png"
}
"lon1" = {
gpu = false
name = "London, United Kingdom"
icon = "/emojis/1f1ec-1f1e7.png"
}
"fra1" = {
gpu = false
name = "Frankfurt, Germany"
icon = "/emojis/1f1e9-1f1ea.png"
}
"tor1" = {
gpu = true
name = "Toronto, Canada"
icon = "/emojis/1f1e8-1f1e6.png"
}
"blr1" = {
gpu = false
name = "Bangalore, India"
icon = "/emojis/1f1ee-1f1f3.png"
}
"syd1" = {
gpu = false
name = "Sydney, Australia"
icon = "/emojis/1f1e6-1f1fa.png"
}
"atl1" = {
gpu = false
name = "Atlanta, USA"
icon = "/emojis/1f1fa-1f1f8.png"
}
# Legacy/Restricted datacenters (not recommended for new workloads)
"nyc2" = {
gpu = true # GPU available but restricted to existing users
name = "New York City, USA (Legacy)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sfo2" = {
gpu = false # No GPU available per current regional availability
name = "San Francisco, USA (Legacy SFO2)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"sfo1" = {
gpu = false # No GPU in legacy datacenter
name = "San Francisco, USA (Legacy SFO1)"
icon = "/emojis/1f1fa-1f1f8.png"
}
"ams2" = {
gpu = false # No GPU in legacy datacenter
name = "Amsterdam, Netherlands (Legacy)"
icon = "/emojis/1f1f3-1f1f1.png"
}
}
}
locals {
allowed_regions = data.coder_parameter.gpu_only.value ? [for k, v in local.zones : k if v.gpu] : keys(local.zones)
default_region = data.coder_parameter.gpu_only.value ? (length([for k, v in local.zones : k if v.gpu]) > 0 ? [for k, v in local.zones : k if v.gpu][0] : null) : (var.default != null && var.default != "" ? var.default : keys(local.zones)[0])
}
data "coder_parameter" "region" {
name = "digitalocean_region"
display_name = var.display_name
description = var.description
icon = "/icon/digital-ocean.svg"
mutable = var.mutable
form_type = "radio"
default = local.default_region
order = var.coder_parameter_order
dynamic "option" {
for_each = {
for k, v in local.zones : k => v
if contains(local.allowed_regions, k)
}
content {
icon = try(var.custom_icons[option.key], option.value.icon)
name = try(var.custom_names[option.key], option.value.name)
description = option.key
value = option.key
}
}
}
output "value" {
description = "DigitalOcean region identifier."
value = data.coder_parameter.region.value
}
@@ -0,0 +1,47 @@
---
display_name: Linode Instance (Linux)
description: Provision Linode instances as Coder workspaces
icon: ../../../../.icons/akamai.svg
verified: false
tags: [vm, linux, linode]
---
# Remote Development on Linode Instances
Provision Linode instances as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
<!-- TODO: Add screenshot -->
## Prerequisites
To deploy workspaces as Linode instances, you'll need:
- Linode [personal access token (PAT)](https://www.linode.com/docs/products/tools/api/guides/manage-api-tokens/)
### Authentication
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Linode.
Obtain a [Linode Personal Access Token](https://cloud.linode.com/profile/tokens) and set the `linode_token` variable when deploying the template.
For other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/linode/linode/latest/docs).
## Features
- **Multiple Instance Types**: From Nanode 1GB to 32GB configurations
- **Comprehensive OS Support**: Ubuntu, Debian, CentOS, Fedora, AlmaLinux, Rocky Linux
- **Global Regions**: 32 Linode regions across North America, Europe, Asia-Pacific, South America, and Australia
- **Persistent Storage**: Configurable volumes (10GB-1TB) that persist `$HOME` across workspace restarts
- **Development Tools**: Pre-configured with VS Code Server
- **Monitoring**: Built-in CPU, memory, and disk usage monitoring
## Architecture
This template provisions the following resources:
- Linode instance (ephemeral, deleted on stop)
- Linode volume (persistent, mounted to `/home/coder`)
This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script).
> [!NOTE]
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,56 @@
#cloud-config
hostname: ${hostname}
users:
- name: ${username}
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
groups: sudo
shell: /bin/bash
packages:
- git
- curl
- wget
- unzip
disk_setup:
/dev/sdb:
table_type: 'gpt'
layout: true
overwrite: false
fs_setup:
- label: ${home_volume_label}
filesystem: ext4
device: /dev/sdb
partition: auto
mounts:
- ["/dev/sdb", "/home/${username}", "ext4", "defaults", "0", "2"]
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=-1000
SyslogIdentifier=coder-agent
[Install]
WantedBy=multi-user.target
runcmd:
- mkdir -p /home/${username}
- chown ${username}:${username} /home/${username}
- systemctl enable coder-agent
- systemctl start coder-agent
+397
View File
@@ -0,0 +1,397 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
linode = {
source = "linode/linode"
}
}
}
provider "coder" {}
# Variable for Linode API token
variable "linode_token" {
description = "Linode API token for authentication"
type = string
sensitive = true
default = ""
}
# Configure the Linode Provider
provider "linode" {
token = var.linode_token != "" ? var.linode_token : null
}
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)}"
}
}
locals {
vm_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
root_disk_label = substr("${local.vm_name}-root", 0, 32)
home_volume_label = substr("${local.vm_name}-home", 0, 32)
}
# 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"
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
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 = "us-east"
mutable = false
option {
name = "Newark, NJ (US East)"
value = "us-east"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Washington, DC (US East)"
value = "us-iad"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Fremont, CA (US West)"
value = "us-west"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Los Angeles, CA (US West)"
value = "us-lax"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Dallas, TX (US Central)"
value = "us-central"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Chicago, IL (US Central)"
value = "us-ord"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Atlanta, GA (US Southeast)"
value = "us-southeast"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Miami, FL (US Southeast)"
value = "us-mia"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Seattle, WA (US West)"
value = "us-sea"
icon = "/emojis/1f1fa-1f1f8.png"
}
option {
name = "Toronto, CA"
value = "ca-central"
icon = "/emojis/1f1e8-1f1e6.png"
}
option {
name = "London, UK"
value = "eu-west"
icon = "/emojis/1f1ec-1f1e7.png"
}
option {
name = "London 2, UK"
value = "gb-lon"
icon = "/emojis/1f1ec-1f1e7.png"
}
option {
name = "Frankfurt, DE"
value = "eu-central"
icon = "/emojis/1f1e9-1f1ea.png"
}
option {
name = "Frankfurt 2, DE"
value = "de-fra-2"
icon = "/emojis/1f1e9-1f1ea.png"
}
option {
name = "Paris, FR"
value = "fr-par"
icon = "/emojis/1f1eb-1f1f7.png"
}
option {
name = "Amsterdam, NL"
value = "nl-ams"
icon = "/emojis/1f1f3-1f1f1.png"
}
option {
name = "Stockholm, SE"
value = "se-sto"
icon = "/emojis/1f1f8-1f1ea.png"
}
option {
name = "Madrid, ES"
value = "es-mad"
icon = "/emojis/1f1ea-1f1f8.png"
}
option {
name = "Milan, IT"
value = "it-mil"
icon = "/emojis/1f1ee-1f1f9.png"
}
option {
name = "Singapore, SG"
value = "ap-south"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Singapore 2, SG"
value = "sg-sin-2"
icon = "/emojis/1f1f8-1f1ec.png"
}
option {
name = "Tokyo 2, JP"
value = "ap-northeast"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Tokyo 3, JP"
value = "jp-tyo-3"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Osaka, JP"
value = "jp-osa"
icon = "/emojis/1f1ef-1f1f5.png"
}
option {
name = "Sydney, AU"
value = "ap-southeast"
icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Melbourne, AU"
value = "au-mel"
icon = "/emojis/1f1e6-1f1fa.png"
}
option {
name = "Mumbai, IN"
value = "ap-west"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Mumbai 2, IN"
value = "in-bom-2"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Chennai, IN"
value = "in-maa"
icon = "/emojis/1f1ee-1f1f3.png"
}
option {
name = "Jakarta, ID"
value = "id-cgk"
icon = "/emojis/1f1ee-1f1e9.png"
}
option {
name = "Sao Paulo, BR"
value = "br-gru"
icon = "/emojis/1f1e7-1f1f7.png"
}
}
data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance Type"
description = "Which Linode instance type would you like to use?"
default = "g6-nanode-1"
type = "string"
icon = "/icon/memory.svg"
mutable = false
option {
name = "Nanode 1GB (1 vCPU, 1 GB RAM)"
value = "g6-nanode-1"
}
option {
name = "Linode 2GB (1 vCPU, 2 GB RAM)"
value = "g6-standard-1"
}
option {
name = "Linode 4GB (2 vCPU, 4 GB RAM)"
value = "g6-standard-2"
}
option {
name = "Linode 8GB (4 vCPU, 8 GB RAM)"
value = "g6-standard-4"
}
option {
name = "Linode 16GB (6 vCPU, 16 GB RAM)"
value = "g6-standard-6"
}
option {
name = "Linode 32GB (8 vCPU, 32 GB RAM)"
value = "g6-standard-8"
}
}
data "coder_parameter" "instance_image" {
name = "instance_image"
display_name = "Instance Image"
description = "Which Linode image would you like to use?"
default = "linode/ubuntu24.04"
type = "string"
mutable = false
option {
name = "Ubuntu 24.04 LTS"
value = "linode/ubuntu24.04"
icon = "/icon/ubuntu.svg"
}
option {
name = "Debian 13"
value = "linode/debian13"
icon = "/icon/debian.svg"
}
option {
name = "Fedora 42"
value = "linode/fedora42"
icon = "/icon/fedora.svg"
}
option {
name = "AlmaLinux 9"
value = "linode/almalinux9"
icon = "/icon/almalinux.svg"
}
option {
name = "Rocky Linux 9"
value = "linode/rocky9"
icon = "/icon/rockylinux.svg"
}
}
data "coder_parameter" "home_volume_size" {
name = "home_volume_size"
display_name = "Home Volume Size (GB)"
description = "How large would you like your home volume to be (in GB)?"
type = "number"
default = 20
mutable = true
validation {
min = 10
max = 1024
monotonic = "increasing"
}
}
resource "linode_volume" "home_volume" {
label = local.home_volume_label
size = data.coder_parameter.home_volume_size.value
region = data.coder_parameter.region.value
# Protect the volume from being deleted due to changes in attributes.
lifecycle {
ignore_changes = all
}
}
resource "linode_instance" "workspace" {
count = data.coder_workspace.me.start_count
label = local.vm_name
region = data.coder_parameter.region.value
type = data.coder_parameter.instance_type.value
private_ip = true
metadata {
user_data = base64encode(templatefile("cloud-init/cloud-config.yaml.tftpl", {
hostname = local.vm_name
username = lower(data.coder_workspace_owner.me.name)
home_volume_label = linode_volume.home_volume.label
init_script = base64encode(coder_agent.main.init_script)
coder_agent_token = coder_agent.main.token
}))
}
tags = ["coder", "workspace", lower(data.coder_workspace_owner.me.name), lower(data.coder_workspace.me.name)]
}
# Create root disk
resource "linode_instance_disk" "root" {
count = data.coder_workspace.me.start_count
label = "boot"
linode_id = linode_instance.workspace[0].id
size = 25000 # 25GB boot disk
image = data.coder_parameter.instance_image.value
}
# Create instance configuration with volume attached
resource "linode_instance_config" "workspace" {
count = data.coder_workspace.me.start_count
label = "${local.vm_name}-config"
linode_id = linode_instance.workspace[0].id
device {
device_name = "sda"
disk_id = linode_instance_disk.root[0].id
}
device {
device_name = "sdb"
volume_id = linode_volume.home_volume.id
}
root_device = "/dev/sda"
kernel = "linode/latest-64bit"
booted = true
}
resource "coder_metadata" "workspace-info" {
count = data.coder_workspace.me.start_count
resource_id = linode_instance.workspace[0].id
item {
key = "region"
value = linode_instance.workspace[0].region
}
item {
key = "type"
value = linode_instance.workspace[0].type
}
}
+22
View File
@@ -29,6 +29,28 @@ if [ -d "registry/$NAMESPACE/modules/$MODULE_NAME" ]; then
echo "Please choose a different name"
exit 1
fi
# Create namespace directory if it doesn't exist
if [ ! -d "registry/$NAMESPACE" ]; then
echo "Creating namespace directory: registry/$NAMESPACE"
mkdir -p "registry/$NAMESPACE"
# Create namespace README if it doesn't exist
if [ ! -f "registry/$NAMESPACE/README.md" ]; then
echo "Creating namespace README: registry/$NAMESPACE/README.md"
cp "examples/namespace/README.md" "registry/$NAMESPACE/README.md"
# Replace NAMESPACE_NAME placeholder in the README
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s/NAMESPACE_NAME/${NAMESPACE}/g" "registry/$NAMESPACE/README.md"
else
# Linux
sed -i "s/NAMESPACE_NAME/${NAMESPACE}/g" "registry/$NAMESPACE/README.md"
fi
fi
fi
mkdir -p "registry/${NAMESPACE}/modules/${MODULE_NAME}"
# Copy required files from the example module
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# This script creates a new sample template directory with required files
# Run it like: ./scripts/new_template.sh my-namespace/my-template
TEMPLATE_ARG=$1
# Check if they are in the root directory
if [ ! -d "registry" ]; then
echo "Please run this script from the root directory of the repository"
echo "Usage: ./scripts/new_template.sh <namespace>/<template_name>"
exit 1
fi
# check if template name is in the format <namespace>/<template_name>
if ! [[ "$TEMPLATE_ARG" =~ ^[a-z0-9_-]+/[a-z0-9_-]+$ ]]; then
echo "Template name must be in the format <namespace>/<template_name>"
echo "Usage: ./scripts/new_template.sh <namespace>/<template_name>"
exit 1
fi
# Extract the namespace and template name
NAMESPACE=$(echo "$TEMPLATE_ARG" | cut -d'/' -f1)
TEMPLATE_NAME=$(echo "$TEMPLATE_ARG" | cut -d'/' -f2)
# Check if the template already exists
if [ -d "registry/$NAMESPACE/templates/$TEMPLATE_NAME" ]; then
echo "Template at registry/$NAMESPACE/templates/$TEMPLATE_NAME already exists"
echo "Please choose a different name"
exit 1
fi
# Create namespace directory if it doesn't exist
if [ ! -d "registry/$NAMESPACE" ]; then
echo "Creating namespace directory: registry/$NAMESPACE"
mkdir -p "registry/$NAMESPACE"
# Create namespace README if it doesn't exist
if [ ! -f "registry/$NAMESPACE/README.md" ]; then
echo "Creating namespace README: registry/$NAMESPACE/README.md"
cp "examples/namespace/README.md" "registry/$NAMESPACE/README.md"
# Replace NAMESPACE_NAME placeholder in the README
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s/NAMESPACE_NAME/${NAMESPACE}/g" "registry/$NAMESPACE/README.md"
else
# Linux
sed -i "s/NAMESPACE_NAME/${NAMESPACE}/g" "registry/$NAMESPACE/README.md"
fi
fi
fi
# Create the template directory structure
mkdir -p "registry/${NAMESPACE}/templates/${TEMPLATE_NAME}"
# Copy required files from the example template
cp -r examples/templates/* "registry/${NAMESPACE}/templates/${TEMPLATE_NAME}/"
# Change to template directory
cd "registry/${NAMESPACE}/templates/${TEMPLATE_NAME}"
# Detect OS and replace placeholders
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s/TEMPLATE_NAME/${TEMPLATE_NAME}/g" main.tf
sed -i '' "s/TEMPLATE_NAME/${TEMPLATE_NAME}/g" README.md
else
# Linux
sed -i "s/TEMPLATE_NAME/${TEMPLATE_NAME}/g" main.tf
sed -i "s/TEMPLATE_NAME/${TEMPLATE_NAME}/g" README.md
fi
echo "Template scaffolded successfully at registry/${NAMESPACE}/templates/${TEMPLATE_NAME}"
echo "Next steps:"
echo "1. Edit main.tf to add your infrastructure resources"
echo "2. Update README.md with template-specific information"
echo "3. Test your template with 'coder templates push'"
echo "4. Consider adding an icon at .icons/${TEMPLATE_NAME}.svg"