Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9d44ca338 | |||
| 7152b85246 | |||
| 41c6bece3e | |||
| 9452763f7d | |||
| 77328656ff | |||
| c4c484089f | |||
| 7e53098bea | |||
| 901043bb01 | |||
| 35e64f2e4a | |||
| 65edb54e88 | |||
| c270edfdab | |||
| f712d1c55b | |||
| bc383a32f3 | |||
| a9b015044f | |||
| e94dfd2df6 | |||
| 9125a52f57 |
@@ -0,0 +1 @@
|
||||
../AGENTS.md
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 451 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: Jash
|
||||
bio: Coder user and contributor.
|
||||
github: AJ0070
|
||||
avatar: ./.images/avatar.png
|
||||
status: community
|
||||
---
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
@@ -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))
|
||||
)
|
||||
])
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 302 KiB |
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
```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"
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 DigitalOcean’s 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||