1200 lines
36 KiB
Markdown
1200 lines
36 KiB
Markdown
# Coder Modules:
|
|
|
|
_Technical reference for understanding, developing, and contributing Coder modules_
|
|
|
|
## Table of Contents
|
|
|
|
1. [Foundation & Core Concepts](#foundation--core-concepts)
|
|
2. [How Coder Modules Work](#how-coder-modules-work)
|
|
3. [Module Architecture & Registry](#module-architecture--registry)
|
|
4. [Module Implementation Patterns](#module-implementation-patterns)
|
|
5. [Reference & Resources](#reference--resources)
|
|
|
|
---
|
|
|
|
## Foundation & Core Concepts
|
|
|
|
### What are Coder Modules?
|
|
|
|
Coder modules are reusable Terraform configurations that extend Coder workspaces with development tools, integrations, and functionality. Each module encapsulates a specific capability and can be integrated into workspace templates to provide targeted functionality.
|
|
|
|
Modules are building blocks that add functionality to Coder workspaces. Instead of manually configuring development tools, you include a module and it handles the setup automatically.
|
|
|
|
### Key Features
|
|
|
|
Modules provide several key features that make Coder workspaces powerful and scalable:
|
|
|
|
**Reusability**: Write once, use everywhere. Modules eliminate configuration duplication across multiple templates and environments.
|
|
|
|
**Consistency**: Standardized implementations ensure uniform development environments and enforce best practices across teams.
|
|
|
|
**Composability**: Mix and match modules to create comprehensive development environments tailored to specific needs.
|
|
|
|
**Maintainability**: Centralized module development means bug fixes and improvements benefit all users, with controlled updates ensuring stability and predictable change management.
|
|
|
|
**Speed**: Automated setup and configuration reduces manual overhead and accelerates development environment creation.
|
|
|
|
### Ecosystem Integration
|
|
|
|
Coder modules operate within a structured ecosystem:
|
|
|
|
- **Coder Platform**: Core infrastructure management and workspace orchestration
|
|
- **Templates**: Terraform configurations defining workspace infrastructure and module integration
|
|
- **Modules**: Reusable components providing specific functionality extensions
|
|
- **Registry**: Centralized repository for module discovery and distribution
|
|
- **Agents**: Runtime components executing module-defined operations within workspaces
|
|
|
|
The relationship between these components enables scalable workspace management:
|
|
|
|
```
|
|
┌─────────────────────────┐
|
|
│ Coder Platform │
|
|
│ │
|
|
│ Orchestrates entire │
|
|
│ workspace lifecycle │
|
|
└───────────┬─────────────┘
|
|
│
|
|
│ Manages
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ Module Registry │◄─── Publishes
|
|
│ │ modules
|
|
│ Hosts and distributes │
|
|
│ reusable modules │
|
|
└───────────┬─────────────┘
|
|
│
|
|
│ Provides modules
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ Templates │◄─── Users create
|
|
│ │ templates
|
|
│ Define infrastructure │
|
|
│ and integrate modules │
|
|
└───────────┬─────────────┘
|
|
│
|
|
│ Provisions
|
|
▼
|
|
┌─────────────────────────┐
|
|
│ Workspaces │◄─── Developers use
|
|
│ │ workspaces
|
|
│ Running development │
|
|
│ environments │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
### Terraform Foundation
|
|
|
|
Coder modules are written in Terraform and are used to extend Coder Templates. The [Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs) extends standard Terraform functionality with workspace-specific resources:
|
|
|
|
**Core Module Resources:**
|
|
|
|
- [`coder_script`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script): Executes scripts during workspace lifecycle events
|
|
- [`coder_app`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/app): Creates accessible applications within the workspace interface
|
|
- [`coder_env`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env): Sets environment variables in the workspace
|
|
|
|
**Template Resources (rarely used in modules):**
|
|
|
|
- [`coder_agent`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent): Defines agent configuration for workspace runtime
|
|
- [`coder_parameter`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/parameter): Collects user input during workspace provisioning
|
|
- [`coder_metadata`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/metadata): Displays resource information in the workspace interface
|
|
|
|
**Advanced Resources:**
|
|
|
|
- [`coder_agent_instance`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent_instance): Manages agent instance connections
|
|
- [`coder_ai_task`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/ai_task): Defines AI-powered task automation
|
|
- [`coder_devcontainer`](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/devcontainer): Configures development container environments
|
|
|
|
Modules use standard Terraform syntax plus these Coder-specific resources to integrate with workspaces.
|
|
|
|
### Provider and Terraform Requirements
|
|
|
|
Modules specify required Terraform and provider versions:
|
|
|
|
```tf
|
|
terraform {
|
|
required_version = ">= 1.0"
|
|
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
version = ">= 2.5"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
This ensures compatibility with minimum versions of Terraform and the Coder provider.
|
|
|
|
**Current Version Standards:**
|
|
|
|
- **Latest Coder Provider**: v2.8.0 (June 2025)
|
|
- **Recommended Minimum**: `>= 2.5` (used by most current modules)
|
|
- **Legacy Modules**: Some older modules still use lower versions (0.12+) but should be updated when possible
|
|
|
|
New modules should use `>= 2.5` to ensure access to modern features like app groups, prebuilt workspaces, and improved validation.
|
|
|
|
---
|
|
|
|
## How Coder Modules Work
|
|
|
|
### Module Lifecycle
|
|
|
|
Module execution follows a defined lifecycle within workspace operations:
|
|
|
|
1. **Template Processing**: Templates reference modules and define configuration parameters
|
|
2. **Resource Planning**: Terraform analyzes module dependencies and resource requirements
|
|
3. **Resource Provisioning**: Terraform creates infrastructure and workspace resources
|
|
4. **Agent Initialization**: Coder agent starts and processes module-defined scripts
|
|
5. **Runtime Operation**: Modules provide ongoing functionality through applications and integrations
|
|
|
|
### Core Module Components
|
|
|
|
#### Input Variables
|
|
|
|
Modules define input variables to control behavior and configuration:
|
|
|
|
```tf
|
|
variable "agent_id" {
|
|
type = string
|
|
description = "The ID of a Coder agent."
|
|
}
|
|
|
|
variable "port" {
|
|
type = number
|
|
description = "The port to run VS Code Web on."
|
|
default = 13338
|
|
}
|
|
|
|
variable "extensions" {
|
|
type = list(string)
|
|
description = "A list of extensions to install."
|
|
default = []
|
|
}
|
|
|
|
variable "accept_license" {
|
|
type = bool
|
|
description = "Accept the VS Code Server license. https://code.visualstudio.com/license/server"
|
|
default = false
|
|
validation {
|
|
condition = var.accept_license == true
|
|
error_message = "You must accept the VS Code license agreement by setting accept_license=true."
|
|
}
|
|
}
|
|
|
|
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 Validation Patterns
|
|
|
|
Modules use validation blocks to catch configuration errors early:
|
|
|
|
```tf
|
|
# Example from jetbrains-gateway module
|
|
variable "arch" {
|
|
type = string
|
|
description = "The target architecture of the workspace"
|
|
default = "amd64"
|
|
validation {
|
|
condition = contains(["amd64", "arm64"], var.arch)
|
|
error_message = "Architecture must be either 'amd64' or 'arm64'."
|
|
}
|
|
}
|
|
|
|
variable "channel" {
|
|
type = string
|
|
description = "JetBrains IDE release channel. Valid values are release and eap."
|
|
default = "release"
|
|
validation {
|
|
condition = can(regex("^(release|eap)$", var.channel))
|
|
error_message = "The channel must be either release or eap."
|
|
}
|
|
}
|
|
|
|
variable "jetbrains_ides" {
|
|
type = list(string)
|
|
description = "The list of IDE product codes."
|
|
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
|
validation {
|
|
condition = (
|
|
alltrue([
|
|
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code)
|
|
])
|
|
)
|
|
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}."
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Sensitive Variables
|
|
|
|
Modules handling credentials or API keys use sensitive variables:
|
|
|
|
```tf
|
|
# Example from aider module
|
|
variable "ai_api_key" {
|
|
type = string
|
|
description = "API key for the selected AI provider."
|
|
default = ""
|
|
sensitive = true
|
|
}
|
|
|
|
# Example from windows-rdp module
|
|
variable "admin_password" {
|
|
type = string
|
|
default = "coderRDP!"
|
|
sensitive = true
|
|
}
|
|
```
|
|
|
|
#### Multi-line Descriptions
|
|
|
|
Complex variable descriptions use heredoc syntax for better readability:
|
|
|
|
```tf
|
|
# Example from code-server module
|
|
variable "subdomain" {
|
|
type = bool
|
|
description = <<-EOT
|
|
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
|
|
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
|
|
EOT
|
|
default = false
|
|
}
|
|
|
|
# Example from jfrog-oauth module
|
|
variable "package_managers" {
|
|
type = object({
|
|
npm = optional(list(string), [])
|
|
go = optional(list(string), [])
|
|
pypi = optional(list(string), [])
|
|
docker = optional(list(string), [])
|
|
})
|
|
description = <<-EOF
|
|
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
|
For example:
|
|
{
|
|
npm = ["GLOBAL_NPM_REPO_KEY", "@SCOPED:NPM_REPO_KEY"]
|
|
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"]
|
|
}
|
|
EOF
|
|
}
|
|
```
|
|
|
|
#### Resource Definitions
|
|
|
|
Modules create Coder-specific resources to implement functionality:
|
|
|
|
```tf
|
|
resource "coder_script" "vscode-web" {
|
|
agent_id = var.agent_id
|
|
display_name = "VS Code Web"
|
|
icon = "/icon/code.svg"
|
|
script = templatefile("${path.module}/run.sh", {
|
|
PORT : var.port,
|
|
LOG_PATH : var.log_path,
|
|
INSTALL_PREFIX : var.install_prefix,
|
|
EXTENSIONS : join(",", var.extensions),
|
|
TELEMETRY_LEVEL : var.telemetry_level,
|
|
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
|
FOLDER : var.folder,
|
|
SERVER_BASE_PATH : local.server_base_path,
|
|
COMMIT_ID : var.commit_id,
|
|
})
|
|
run_on_start = true
|
|
}
|
|
|
|
resource "coder_app" "vscode-web" {
|
|
agent_id = var.agent_id
|
|
slug = var.slug
|
|
display_name = var.display_name
|
|
url = local.url
|
|
icon = "/icon/code.svg"
|
|
subdomain = var.subdomain
|
|
share = var.share
|
|
order = var.order
|
|
group = var.group
|
|
|
|
healthcheck {
|
|
url = local.healthcheck_url
|
|
interval = 5
|
|
threshold = 6
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Lifecycle Preconditions
|
|
|
|
Advanced modules use lifecycle preconditions to validate configuration at the Terraform level:
|
|
|
|
```tf
|
|
# Example from code-server module
|
|
resource "coder_script" "code-server" {
|
|
agent_id = var.agent_id
|
|
display_name = "code-server"
|
|
script = templatefile("${path.module}/run.sh", local.template_vars)
|
|
run_on_start = true
|
|
|
|
lifecycle {
|
|
precondition {
|
|
condition = !var.offline || length(var.extensions) == 0
|
|
error_message = "Offline mode does not allow extensions to be installed"
|
|
}
|
|
|
|
precondition {
|
|
condition = !var.offline || !var.use_cached
|
|
error_message = "Offline and Use Cached can not be used together"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### External App Resources
|
|
|
|
Some modules create links to external documentation or resources:
|
|
|
|
```tf
|
|
# Example from windows-rdp module
|
|
resource "coder_app" "rdp-docs" {
|
|
agent_id = var.agent_id
|
|
display_name = "Local RDP Docs"
|
|
slug = "rdp-docs"
|
|
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
|
|
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop"
|
|
external = true
|
|
}
|
|
```
|
|
|
|
#### Output Values
|
|
|
|
Most modules don't need output values, but some specialized modules provide them for template integration and inter-module communication. Outputs are typically used by:
|
|
|
|
- **Region selector modules** (for passing selected regions to infrastructure)
|
|
- **Authentication modules** (for sharing tokens/credentials)
|
|
- **Git modules** (for sharing repository information)
|
|
|
|
```tf
|
|
# Example from git-clone module
|
|
output "repo_dir" {
|
|
value = local.clone_path
|
|
description = "Full path of cloned repo directory"
|
|
}
|
|
|
|
output "clone_url" {
|
|
value = local.clone_url
|
|
description = "The exact Git repository URL that will be cloned"
|
|
}
|
|
|
|
output "folder_name" {
|
|
value = local.folder_name
|
|
description = "The name of the folder that will be created"
|
|
}
|
|
|
|
# Example from jetbrains-gateway module
|
|
output "identifier" {
|
|
value = local.identifier
|
|
}
|
|
|
|
output "download_link" {
|
|
value = local.download_link
|
|
}
|
|
|
|
output "url" {
|
|
value = coder_app.gateway.url
|
|
}
|
|
```
|
|
|
|
### Module Integration with Templates
|
|
|
|
Modules are designed to integrate seamlessly with Coder templates, following established patterns for configuration and lifecycle management.
|
|
|
|
#### Parameter Collection
|
|
|
|
Most modules receive user input through variables passed from templates. Only specialized modules (like region selectors) collect parameters directly:
|
|
|
|
```tf
|
|
# Example from gcp-region module
|
|
data "coder_parameter" "region" {
|
|
name = "gcp_region"
|
|
display_name = var.display_name
|
|
description = var.description
|
|
icon = "/icon/gcp.png"
|
|
mutable = var.mutable
|
|
default = var.default != null && var.default != "" && (!var.gpu_only || try(local.zones[var.default].gpu, false)) ? var.default : null
|
|
order = var.coder_parameter_order
|
|
dynamic "option" {
|
|
for_each = {
|
|
for k, v in local.zones : k => v
|
|
if anytrue([for d in var.regions : startswith(k, d)]) && (!var.gpu_only || v.gpu) && (!var.single_zone_per_region || endswith(k, "-a"))
|
|
}
|
|
content {
|
|
icon = try(var.custom_icons[option.key], option.value.icon)
|
|
name = try(var.custom_names[option.key], var.single_zone_per_region ? substr(option.value.name, 0, length(option.value.name) - 4) : option.value.name)
|
|
description = option.key
|
|
value = option.key
|
|
}
|
|
}
|
|
}
|
|
|
|
# Template usage example
|
|
module "gcp_region" {
|
|
source = "registry.coder.com/coder/gcp-region/coder"
|
|
default = "us-central1-a"
|
|
regions = ["us-central1", "us-west1"]
|
|
}
|
|
```
|
|
|
|
**Best Practice**: Define parameters in templates and pass values to modules via variables, rather than having modules collect parameters directly.
|
|
|
|
#### Template Integration Patterns
|
|
|
|
Templates integrate modules using several established patterns:
|
|
|
|
**Conditional Loading**: Templates use `start_count` to conditionally load modules based on workspace state, ensuring modules only run when workspaces are active.
|
|
|
|
**Simple Composition**: Templates include multiple modules independently, each handling specific functionality without complex interdependencies.
|
|
|
|
**Parameter-Driven Configuration**: Templates collect user input via `coder_parameter` and pass values to modules through variables, keeping input collection separate from module implementation.
|
|
|
|
**Version Pinning**: Templates pin module versions using version constraints (e.g., `version = "~> 1.0"`) to ensure stability while allowing compatible updates.
|
|
|
|
### Advanced Module Capabilities
|
|
|
|
#### Common Data Sources
|
|
|
|
Most modules use standard data sources to access workspace context:
|
|
|
|
```tf
|
|
# Essential data sources for workspace context
|
|
data "coder_workspace" "me" {}
|
|
data "coder_workspace_owner" "me" {}
|
|
|
|
# Architecture detection for platform-specific installations
|
|
data "coder_provisioner" "me" {}
|
|
|
|
# Example usage in modules
|
|
locals {
|
|
# Platform-specific binary selection
|
|
arch_suffix = data.coder_provisioner.me.arch == "arm64" ? "-arm64" : ""
|
|
|
|
# Dynamic URL construction
|
|
workspace_url = "https://${data.coder_workspace.me.access_url}"
|
|
|
|
# User-specific configuration
|
|
owner_name = data.coder_workspace_owner.me.name
|
|
}
|
|
|
|
# Example from jetbrains-gateway module - Architecture-based logic
|
|
locals {
|
|
download_key = var.arch == "arm64" ? "linuxARM64" : "linux"
|
|
|
|
# Complex architecture detection and IDE version handling
|
|
effective_jetbrains_ide_versions = {
|
|
for k, v in var.jetbrains_ide_versions : k => {
|
|
build_number = v.build_number
|
|
version = var.arch == "arm64" ? "${v.version}-aarch64" : v.version
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Conditional Logic
|
|
|
|
Modules can use conditional logic for various purposes including URL construction, resource configuration, and feature toggling:
|
|
|
|
```tf
|
|
# Example from vscode-web module
|
|
locals {
|
|
server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s/", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
|
|
url = var.folder == "" ? "http://localhost:${var.port}${local.server_base_path}" : "http://localhost:${var.port}${local.server_base_path}?folder=${var.folder}"
|
|
healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz"
|
|
}
|
|
|
|
# Example from jetbrains-gateway module
|
|
locals {
|
|
download_key = var.arch == "arm64" ? "linuxARM64" : "linux"
|
|
effective_jetbrains_ide_versions = {
|
|
for k, v in var.jetbrains_ide_versions : k => {
|
|
build_number = v.build_number
|
|
version = var.arch == "arm64" ? "${v.version}-aarch64" : v.version
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Complex Local Value Processing
|
|
|
|
Modules often use sophisticated local value processing for data transformation:
|
|
|
|
```tf
|
|
# Example from aider module - Environment variable mapping
|
|
locals {
|
|
provider_env_vars = {
|
|
openai = "OPENAI_API_KEY"
|
|
anthropic = "ANTHROPIC_API_KEY"
|
|
azure = "AZURE_OPENAI_API_KEY"
|
|
google = "GOOGLE_API_KEY"
|
|
cohere = "COHERE_API_KEY"
|
|
mistral = "MISTRAL_API_KEY"
|
|
ollama = "OLLAMA_HOST"
|
|
custom = var.custom_env_var_name
|
|
}
|
|
|
|
env_var_name = local.provider_env_vars[var.ai_provider]
|
|
|
|
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
|
}
|
|
|
|
# Example from jfrog-oauth module - Template processing
|
|
locals {
|
|
npmrc = templatefile(
|
|
"${path.module}/.npmrc.tftpl",
|
|
merge(
|
|
local.common_values,
|
|
{
|
|
REPOS = [
|
|
for r in var.package_managers.npm :
|
|
strcontains(r, ":") ? zipmap(["SCOPE", "NAME"], ["${split(":", r)[0]}:", split(":", r)[1]]) : { SCOPE = "", NAME = r }
|
|
]
|
|
}
|
|
)
|
|
)
|
|
}
|
|
```
|
|
|
|
#### External Integrations
|
|
|
|
Modules can integrate with external services and APIs:
|
|
|
|
```tf
|
|
# Example from jfrog-oauth module
|
|
data "coder_external_auth" "jfrog" {
|
|
id = var.external_auth_id
|
|
}
|
|
|
|
resource "coder_env" "jfrog_ide_access_token" {
|
|
count = var.configure_code_server ? 1 : 0
|
|
agent_id = var.agent_id
|
|
name = "JFROG_IDE_ACCESS_TOKEN"
|
|
value = data.coder_external_auth.jfrog.access_token
|
|
}
|
|
|
|
resource "coder_env" "goproxy" {
|
|
count = length(var.package_managers.go) == 0 ? 0 : 1
|
|
agent_id = var.agent_id
|
|
name = "GOPROXY"
|
|
value = join(",", [
|
|
for repo in var.package_managers.go :
|
|
"https://${local.username}:${data.coder_external_auth.jfrog.access_token}@${local.jfrog_host}/artifactory/api/go/${repo}"
|
|
])
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Module Architecture & Registry
|
|
|
|
### Module Architecture
|
|
|
|
A Coder module implements a standard Terraform module structure with Coder-specific conventions:
|
|
|
|
```
|
|
module-name/
|
|
├── main.tf # Primary Terraform configuration
|
|
├── README.md # Documentation with structured frontmatter
|
|
├── main.test.ts # Automated test suite
|
|
└── run.sh # Optional execution script
|
|
```
|
|
|
|
### Module Registry and Namespaces
|
|
|
|
Modules in the Coder Registry are organized using namespaces, which provide a way to group modules by their maintainers and ensure unique module names across the ecosystem.
|
|
|
|
#### Understanding Namespaces
|
|
|
|
Each namespace represents a distinct contributor or organization:
|
|
|
|
- **`coder`**: The official namespace maintained by the Coder team. All modules in this namespace are verified and maintained with official support.
|
|
- **Community namespaces**: Individual contributors and organizations can create their own namespaces (e.g., `thezoker`, `whizus`, `nataindata`) to publish modules.
|
|
|
|
#### Module Source Format
|
|
|
|
When referencing a module in your template, use the following format:
|
|
|
|
```
|
|
registry.coder.com/[namespace]/[module-name]/coder
|
|
```
|
|
|
|
For example:
|
|
|
|
- Official module: `registry.coder.com/coder/vscode-web/coder`
|
|
- Community module: `registry.coder.com/thezoker/nodejs/coder`
|
|
|
|
The trailing `/coder` is a required constant that indicates this is a Coder-specific module.
|
|
|
|
#### Namespace Directory Structure
|
|
|
|
Within the registry repository, modules are organized as:
|
|
|
|
```
|
|
registry/
|
|
├── coder/ # Official namespace
|
|
│ └── modules/
|
|
│ ├── vscode-web/
|
|
│ ├── cursor/
|
|
│ └── ...
|
|
├── thezoker/ # Community namespace
|
|
│ └── modules/
|
|
│ └── nodejs/
|
|
└── whizus/ # Community namespace
|
|
└── modules/
|
|
├── exoscale-zone/
|
|
└── ...
|
|
```
|
|
|
|
#### Creating Your Own Namespace
|
|
|
|
To contribute modules under your own namespace:
|
|
|
|
1. Fork the registry repository
|
|
2. Create a directory with your GitHub username or organization name under `/registry/`
|
|
3. Add a `modules/` subdirectory for your modules
|
|
4. Each module gets its own directory containing the standard module files
|
|
5. Submit a pull request to have your modules included in the registry
|
|
|
|
#### Namespace Verification
|
|
|
|
- Modules in the `coder` namespace are automatically verified
|
|
- Verification status is indicated in the module's README frontmatter
|
|
|
|
---
|
|
|
|
## Module Implementation Patterns
|
|
|
|
Common patterns for building reliable Coder modules.
|
|
|
|
### Core Implementation Patterns
|
|
|
|
#### The Standard Module Structure
|
|
|
|
Standard structure for Coder modules:
|
|
|
|
```tf
|
|
terraform {
|
|
required_version = ">= 1.0"
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
version = ">= 2.5"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Required variables
|
|
variable "agent_id" {
|
|
type = string
|
|
description = "The ID of a Coder agent."
|
|
}
|
|
|
|
# Optional variables with sensible defaults
|
|
variable "version" {
|
|
type = string
|
|
description = "Version to install."
|
|
default = "latest"
|
|
}
|
|
|
|
# Data sources for workspace context
|
|
data "coder_workspace" "me" {}
|
|
data "coder_workspace_owner" "me" {}
|
|
|
|
# Main functionality
|
|
resource "coder_script" "install" {
|
|
agent_id = var.agent_id
|
|
script = templatefile("${path.module}/install.sh", {
|
|
version = var.version
|
|
})
|
|
}
|
|
|
|
resource "coder_app" "main" {
|
|
count = data.coder_workspace.me.start_count
|
|
agent_id = var.agent_id
|
|
display_name = "Application"
|
|
icon = "/icon/app.svg"
|
|
url = "http://localhost:8080"
|
|
}
|
|
|
|
# Outputs for other modules or templates
|
|
output "app_url" {
|
|
value = try(coder_app.main[0].url, "")
|
|
description = "The URL to access the application."
|
|
}
|
|
```
|
|
|
|
#### Conditional Resource Creation
|
|
|
|
Modules use conditional resource creation based on configuration variables:
|
|
|
|
```tf
|
|
# Example from jfrog-oauth module
|
|
resource "coder_env" "jfrog_ide_url" {
|
|
count = var.configure_code_server ? 1 : 0
|
|
agent_id = var.agent_id
|
|
name = "JFROG_IDE_URL"
|
|
value = var.jfrog_url
|
|
}
|
|
|
|
resource "coder_env" "goproxy" {
|
|
count = length(var.package_managers.go) == 0 ? 0 : 1
|
|
agent_id = var.agent_id
|
|
name = "GOPROXY"
|
|
value = join(",", [
|
|
for repo in var.package_managers.go :
|
|
"https://${local.username}:${data.coder_external_auth.jfrog.access_token}@${local.jfrog_host}/artifactory/api/go/${repo}"
|
|
])
|
|
}
|
|
|
|
# Example from vscode-desktop module
|
|
resource "coder_app" "vscode" {
|
|
agent_id = var.agent_id
|
|
external = true
|
|
icon = "/icon/code.svg"
|
|
slug = "vscode"
|
|
display_name = "VS Code Desktop"
|
|
order = var.order
|
|
group = var.group
|
|
|
|
url = join("", [
|
|
"vscode://coder.coder-remote/open",
|
|
"?owner=",
|
|
data.coder_workspace_owner.me.name,
|
|
"&workspace=",
|
|
data.coder_workspace.me.name,
|
|
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
|
var.open_recent ? "&openRecent" : "",
|
|
"&url=",
|
|
data.coder_workspace.me.access_url,
|
|
"&token=$SESSION_TOKEN",
|
|
])
|
|
}
|
|
```
|
|
|
|
### Advanced Implementation Patterns
|
|
|
|
#### Complex Installation Pattern
|
|
|
|
Complex modules handle multi-step installation within a single script:
|
|
|
|
```tf
|
|
resource "coder_script" "install" {
|
|
agent_id = var.agent_id
|
|
display_name = "Install Application"
|
|
script = templatefile("${path.module}/install.sh", {
|
|
version = var.version
|
|
arch = data.coder_provisioner.me.arch
|
|
workspace_id = data.coder_workspace.me.id
|
|
folder = var.folder
|
|
})
|
|
run_on_start = true
|
|
}
|
|
```
|
|
|
|
#### Service Installation Pattern
|
|
|
|
Modules typically install and configure services during workspace startup:
|
|
|
|
```tf
|
|
resource "coder_script" "install_service" {
|
|
agent_id = var.agent_id
|
|
display_name = "Install Service"
|
|
script = file("${path.module}/install.sh")
|
|
run_on_start = true
|
|
}
|
|
|
|
resource "coder_app" "service" {
|
|
agent_id = var.agent_id
|
|
slug = "service"
|
|
display_name = "Service"
|
|
url = "http://localhost:${var.port}"
|
|
icon = "/icon/service.svg"
|
|
subdomain = true
|
|
|
|
healthcheck {
|
|
url = "http://localhost:${var.port}/health"
|
|
interval = 5
|
|
threshold = 6
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Configuration Template Pattern
|
|
|
|
Modules often need to generate configuration files:
|
|
|
|
```tf
|
|
resource "coder_script" "generate_config" {
|
|
agent_id = var.agent_id
|
|
script = templatefile("${path.module}/generate-config.sh", {
|
|
config_content = templatefile("${path.module}/config.tpl", {
|
|
workspace_name = data.coder_workspace.me.name
|
|
owner_email = data.coder_workspace_owner.me.email
|
|
custom_settings = var.custom_settings
|
|
})
|
|
})
|
|
}
|
|
```
|
|
|
|
#### External Service Integration Pattern
|
|
|
|
Modules that integrate with external services use external authentication:
|
|
|
|
```tf
|
|
variable "external_auth_id" {
|
|
type = string
|
|
description = "External auth ID for the service."
|
|
default = "github"
|
|
}
|
|
|
|
data "coder_external_auth" "service" {
|
|
id = var.external_auth_id
|
|
}
|
|
|
|
resource "coder_script" "setup_integration" {
|
|
agent_id = var.agent_id
|
|
script = templatefile("${path.module}/setup.sh", {
|
|
auth_token = data.coder_external_auth.service.access_token
|
|
service_url = var.service_url
|
|
external_id = data.coder_external_auth.service.id
|
|
})
|
|
run_on_start = true
|
|
}
|
|
```
|
|
|
|
### User Experience Patterns
|
|
|
|
#### Simple Configuration
|
|
|
|
Modules provide sensible defaults to minimize required configuration:
|
|
|
|
```tf
|
|
variable "port" {
|
|
type = number
|
|
description = "Port number for the application."
|
|
default = 8080
|
|
}
|
|
|
|
variable "extensions" {
|
|
type = list(string)
|
|
description = "List of extensions to install."
|
|
default = []
|
|
}
|
|
|
|
# Most configuration is optional with good defaults
|
|
resource "coder_app" "main" {
|
|
agent_id = var.agent_id
|
|
slug = "app"
|
|
display_name = "Application"
|
|
url = "http://localhost:${var.port}"
|
|
icon = "/icon/app.svg"
|
|
subdomain = true
|
|
}
|
|
```
|
|
|
|
#### Accepting Template Parameters
|
|
|
|
Modules typically receive user preferences through variables passed from templates:
|
|
|
|
```tf
|
|
variable "theme" {
|
|
type = string
|
|
description = "UI theme to use."
|
|
default = "dark"
|
|
validation {
|
|
condition = contains(["dark", "light", "auto"], var.theme)
|
|
error_message = "Theme must be dark, light, or auto."
|
|
}
|
|
}
|
|
|
|
variable "extensions" {
|
|
type = list(string)
|
|
description = "List of extensions to install."
|
|
default = ["git", "docker"]
|
|
}
|
|
|
|
# Use the template-provided values
|
|
resource "coder_script" "configure_theme" {
|
|
agent_id = var.agent_id
|
|
script = "configure_app --theme=${var.theme}"
|
|
}
|
|
```
|
|
|
|
Templates handle user input, modules handle implementation.
|
|
|
|
### Error Handling and Resilience Patterns
|
|
|
|
#### Application Health Checks
|
|
|
|
Modules typically include health checks for web applications:
|
|
|
|
```tf
|
|
# Example from vscode-web module
|
|
resource "coder_app" "vscode-web" {
|
|
agent_id = var.agent_id
|
|
slug = var.slug
|
|
display_name = var.display_name
|
|
url = local.url
|
|
icon = "/icon/code.svg"
|
|
subdomain = var.subdomain
|
|
share = var.share
|
|
order = var.order
|
|
group = var.group
|
|
|
|
healthcheck {
|
|
url = local.healthcheck_url
|
|
interval = 5
|
|
threshold = 6
|
|
}
|
|
}
|
|
|
|
# Example from windows-rdp module
|
|
resource "coder_app" "windows-rdp" {
|
|
agent_id = var.agent_id
|
|
share = var.share
|
|
slug = "web-rdp"
|
|
display_name = "Web RDP"
|
|
url = "http://localhost:7171"
|
|
icon = "/icon/desktop.svg"
|
|
subdomain = true
|
|
order = var.order
|
|
group = var.group
|
|
|
|
healthcheck {
|
|
url = "http://localhost:7171"
|
|
interval = 5
|
|
threshold = 15
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Reference & Resources
|
|
|
|
Quick reference for module development and usage.
|
|
|
|
### Quick Reference Guide
|
|
|
|
#### Essential Module Structure
|
|
|
|
Every Coder module follows this basic structure:
|
|
|
|
```
|
|
module-name/
|
|
├── main.tf # Core Terraform configuration
|
|
├── README.md # Documentation with frontmatter
|
|
├── main.test.ts # Test suite (required)
|
|
├── run.sh # Optional execution script
|
|
└── assets/ # Optional additional files
|
|
├── scripts/
|
|
├── configs/
|
|
└── templates/
|
|
```
|
|
|
|
#### Required Frontmatter Format
|
|
|
|
All module READMEs must include proper frontmatter:
|
|
|
|
```yaml
|
|
---
|
|
display_name: Module Display Name
|
|
description: Brief description of module functionality
|
|
icon: ../../../../.icons/module-icon.svg
|
|
maintainer_github: github-username
|
|
verified: false # true for official modules
|
|
tags: [tag1, tag2, tag3]
|
|
---
|
|
```
|
|
|
|
#### Common Module Patterns
|
|
|
|
**Basic Module Template:**
|
|
|
|
```tf
|
|
terraform {
|
|
required_version = ">= 1.0"
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
version = ">= 2.5"
|
|
}
|
|
}
|
|
}
|
|
|
|
variable "agent_id" {
|
|
type = string
|
|
description = "The ID of a Coder agent."
|
|
}
|
|
|
|
variable "port" {
|
|
type = number
|
|
description = "The port to run JupyterLab on."
|
|
default = 19999
|
|
}
|
|
|
|
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'."
|
|
}
|
|
}
|
|
|
|
data "coder_workspace" "me" {}
|
|
data "coder_workspace_owner" "me" {}
|
|
|
|
resource "coder_script" "jupyterlab" {
|
|
agent_id = var.agent_id
|
|
display_name = "jupyterlab"
|
|
icon = "/icon/jupyter.svg"
|
|
script = templatefile("${path.module}/run.sh", {
|
|
LOG_PATH : var.log_path,
|
|
PORT : var.port
|
|
BASE_URL : var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
|
|
})
|
|
run_on_start = true
|
|
}
|
|
|
|
resource "coder_app" "jupyterlab" {
|
|
agent_id = var.agent_id
|
|
slug = "jupyterlab"
|
|
display_name = "JupyterLab"
|
|
url = var.subdomain ? "http://localhost:${var.port}" : "http://localhost:${var.port}/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
|
|
icon = "/icon/jupyter.svg"
|
|
subdomain = var.subdomain
|
|
share = var.share
|
|
order = var.order
|
|
group = var.group
|
|
}
|
|
```
|
|
|
|
### Development Resources
|
|
|
|
#### Testing Framework
|
|
|
|
The registry uses Bun for testing with specific patterns:
|
|
|
|
```typescript
|
|
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
runTerraformApply,
|
|
runTerraformInit,
|
|
testRequiredVariables,
|
|
} from "~test";
|
|
|
|
describe("module-name", async () => {
|
|
await runTerraformInit(import.meta.dir);
|
|
|
|
testRequiredVariables(import.meta.dir, {
|
|
agent_id: "test-agent",
|
|
});
|
|
|
|
it("creates expected resources", async () => {
|
|
const state = await runTerraformApply(import.meta.dir, {
|
|
agent_id: "test-agent",
|
|
});
|
|
|
|
// Test assertions
|
|
expect(state.resources).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
#### Validation Tools
|
|
|
|
Use the built-in validation tools:
|
|
|
|
```bash
|
|
# Format code
|
|
bun run fmt
|
|
|
|
# Validate Terraform
|
|
bun run terraform-validate
|
|
|
|
# Run tests
|
|
bun test
|
|
|
|
# Test specific module
|
|
bun test -t "module-name"
|
|
```
|
|
|
|
#### Documentation Standards
|
|
|
|
Documentation requirements:
|
|
|
|
- Write clear descriptions and examples
|
|
- Use consistent formatting
|
|
- Keep docs updated with code changes
|
|
- Explain why, not just what
|
|
|
|
### Module Registry Resources
|
|
|
|
#### Official Registry
|
|
|
|
- **Registry Website**: [https://registry.coder.com](https://registry.coder.com)
|
|
- **GitHub Repository**: [https://github.com/coder/registry](https://github.com/coder/registry)
|
|
- **Contributing Guide**: [CONTRIBUTING.md](https://github.com/coder/registry/blob/main/CONTRIBUTING.md)
|
|
|
|
#### Coder Documentation
|
|
|
|
- **Terraform Modules Guide**: [https://coder.com/docs/admin/templates/extending-templates/modules](https://coder.com/docs/admin/templates/extending-templates/modules)
|
|
- **Template Creation**: [https://coder.com/docs/tutorials/template-from-scratch](https://coder.com/docs/tutorials/template-from-scratch)
|
|
- **Coder Provider Documentation**: Available in the Terraform Registry
|
|
|
|
#### Community Resources
|
|
|
|
- **Discord Community**: [https://discord.gg/coder](https://discord.gg/coder)
|
|
- **GitHub Discussions**: Repository discussions for questions and ideas
|
|
- **Community Examples**: Template examples in the main Coder repository
|
|
|
|
### Module Versioning
|
|
|
|
#### Semantic Versioning
|
|
|
|
Modules follow semantic versioning (MAJOR.MINOR.PATCH):
|
|
|
|
- **Patch** (1.2.3 → 1.2.4): Bug fixes that don't affect the API
|
|
- **Minor** (1.2.3 → 1.3.0): New features, backward-compatible changes
|
|
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
|
|
|
|
#### Version Bumping
|
|
|
|
When modifying a module, use the version bump script:
|
|
|
|
```bash
|
|
# For bug fixes
|
|
./.github/scripts/version-bump.sh patch
|
|
|
|
# For new features
|
|
./.github/scripts/version-bump.sh minor
|
|
|
|
# For breaking changes
|
|
./.github/scripts/version-bump.sh major
|
|
```
|
|
|
|
The script automatically detects changed modules, calculates new version numbers, and updates README files.
|
|
|
|
---
|
|
|
|
_This document represents the current state of Coder modules and will continue to evolve as the ecosystem grows. For the most up-to-date information, always refer to the official Coder documentation and registry._
|
|
|
|
**Version**: 1.0
|
|
**Last Updated**: June 2025
|
|
**Maintained by**: Coder Team |