feat: add Quickstart template with language and IDE selection (#24904)

Add a new Quickstart starter template that lets users pick programming
languages, editors, and an optional Git repo to clone. The template uses
Docker under the hood but presents a developer-focused experience: pick
your tools, start coding.

## What's included

- **Languages parameter** (multi-select): Python, Node.js, Go, Rust,
Java, C/C++
- **IDEs parameter** (multi-select): VS Code (Browser), VS Code Desktop,
Cursor, JetBrains, Zed, Windsurf
- **Git repo parameter**: Optional URL to clone on workspace start
- **JetBrains filtering**: Maps selected languages to relevant IDE codes
(Python → PyCharm, Go → GoLand, etc.)
- **Docker precondition check**: Uses `data "external"` +
`terraform_data` precondition to surface a friendly error when Docker is
unavailable, before the Docker provider fails with a cryptic message
- **4 presets**: Web Development, Backend (Go), Data Science, Full Stack
- **Single install script**: All languages install in one `coder_script`
to avoid apt-get lock conflicts (agent scripts run in parallel via
`errgroup`)

<details><summary>Design decisions</summary>

- **Docker as invisible backend**: Docker is required on the Coder
server but never mentioned in the user-facing parameter UI. The
experience is entirely "pick languages, pick editors, start coding."
- **`coder_script` over startup_script**: Language installs use a
templated script file (`install-languages.sh.tftpl`) driven by the
languages parameter. A single script avoids dpkg lock contention since
`coder_script` resources execute concurrently.
- **`data "external"` for Docker check**: The external provider probes
Docker availability independently of the Docker provider. If Docker is
down, the `terraform_data` precondition fails with a human-readable
message before any `docker_*` resource is evaluated. This depends on the
Docker provider connecting lazily (at resource eval time, not at
provider init), which current behavior confirms.
- **JetBrains filtering by language**: Rather than showing all 9
JetBrains IDEs, the template computes relevant IDE codes from the
language selection (e.g. Python → PY, Go → GO) and passes them as
`default` to the JetBrains module.
- **Arch-aware Go install**: The install script detects `uname -m` to
download the correct Go binary for amd64 or arm64.

</details>

<details><summary>Screenshots and recordings from the UI</summary>
<p>
<img width="1851" height="1471" alt="Screenshot 2026-05-05 at 2 14
20 PM"
src="https://github.com/user-attachments/assets/d4c9cdc5-d311-43a5-9e2e-f90b0019eda7"
/>
<img width="1851" height="1471" alt="Screenshot 2026-05-05 at 2 15
06 PM"
src="https://github.com/user-attachments/assets/cf3023fe-b6db-4503-a6c4-eaa0ec0659f8"
/>


https://github.com/user-attachments/assets/7507fd7d-ddb5-457a-9f7d-cbf89b36eb20


</p>
</details> 

> [!NOTE]
> This PR was authored by Coder Agents.
This commit is contained in:
Nick Vigilante
2026-05-06 09:55:38 -04:00
committed by GitHub
parent 3d03c393d2
commit 369a191972
10 changed files with 673 additions and 51 deletions
+450
View File
@@ -0,0 +1,450 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
}
external = {
source = "hashicorp/external"
}
}
}
variable "docker_socket" {
default = ""
description = "(Optional) Docker socket URI"
type = string
}
provider "docker" {
host = var.docker_socket != "" ? var.docker_socket : null
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
# --- Parameters ---
data "coder_parameter" "languages" {
name = "languages"
display_name = "Programming Languages"
description = "Select the languages to pre-install in your workspace"
type = "list(string)"
form_type = "multi-select"
default = jsonencode(["python"])
mutable = true
icon = "/icon/code.svg"
order = 1
option {
name = "Python"
value = "python"
icon = "/icon/python.svg"
}
option {
name = "Node.js"
value = "nodejs"
icon = "/icon/nodejs.svg"
}
option {
name = "Go"
value = "go"
icon = "/icon/go.svg"
}
option {
name = "Rust"
value = "rust"
icon = "/icon/rust.svg"
}
option {
name = "Java"
value = "java"
icon = "/icon/java.svg"
}
option {
name = "C/C++"
value = "cpp"
icon = "/icon/cpp.svg"
}
}
data "coder_parameter" "ides" {
name = "ides"
display_name = "IDEs & Editors"
description = "Select the development environments for your workspace"
type = "list(string)"
form_type = "multi-select"
default = jsonencode(["code-server"])
mutable = true
icon = "/icon/code.svg"
order = 2
option {
name = "VS Code (Browser)"
value = "code-server"
icon = "/icon/code.svg"
}
option {
name = "VS Code Desktop"
value = "vscode-desktop"
icon = "/icon/code.svg"
}
option {
name = "Cursor"
value = "cursor"
icon = "/icon/cursor.svg"
}
option {
name = "JetBrains IDEs"
value = "jetbrains"
icon = "/icon/jetbrains.svg"
}
option {
name = "Zed"
value = "zed"
icon = "/icon/zed.svg"
}
option {
name = "Windsurf"
value = "windsurf"
icon = "/icon/windsurf.svg"
}
}
# Shown only when "JetBrains IDEs" is selected in the IDEs parameter.
# Pre-selects IDEs that match the chosen languages.
data "coder_parameter" "jetbrains_ides" {
count = contains(local.ides, "jetbrains") ? 1 : 0
name = "jetbrains_ides"
display_name = "JetBrains IDEs"
description = "Select the JetBrains IDEs to install"
type = "list(string)"
form_type = "multi-select"
default = jsonencode(local.jetbrains_ides_from_languages)
mutable = true
icon = "/icon/jetbrains.svg"
order = 3
option {
name = "IntelliJ IDEA"
value = "IU"
icon = "/icon/intellij.svg"
}
option {
name = "PyCharm"
value = "PY"
icon = "/icon/pycharm.svg"
}
option {
name = "GoLand"
value = "GO"
icon = "/icon/goland.svg"
}
option {
name = "WebStorm"
value = "WS"
icon = "/icon/webstorm.svg"
}
option {
name = "RustRover"
value = "RR"
icon = "/icon/rustrover.svg"
}
option {
name = "CLion"
value = "CL"
icon = "/icon/clion.svg"
}
option {
name = "PhpStorm"
value = "PS"
icon = "/icon/phpstorm.svg"
}
option {
name = "RubyMine"
value = "RM"
icon = "/icon/rubymine.svg"
}
option {
name = "Rider"
value = "RD"
icon = "/icon/rider.svg"
}
}
data "coder_parameter" "git_repo" {
name = "git_repo"
display_name = "Git Repository (Optional)"
description = "URL of a Git repository to clone into your workspace (leave empty to skip)"
type = "string"
default = ""
mutable = true
icon = "/icon/git.svg"
order = 4
}
# --- Locals ---
locals {
username = data.coder_workspace_owner.me.name
languages = jsondecode(data.coder_parameter.languages.value)
ides = jsondecode(data.coder_parameter.ides.value)
# Map selected languages to the relevant JetBrains IDE product codes.
# Used as the default for the JetBrains IDE selector parameter.
jetbrains_by_language = {
python = ["PY"]
go = ["GO"]
java = ["IU"]
nodejs = ["WS"]
rust = ["RR"]
cpp = ["CL"]
}
jetbrains_ides_from_languages = distinct(flatten([
for lang in local.languages : lookup(local.jetbrains_by_language, lang, [])
]))
# The actual JetBrains IDEs to install, from the user's selection
# in the conditional JetBrains parameter (or empty if not shown).
jetbrains_selected = contains(local.ides, "jetbrains") ? jsondecode(data.coder_parameter.jetbrains_ides[0].value) : []
}
# --- Agent ---
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
startup_script = <<-EOT
set -e
if [ ! -f ~/.init_done ]; then
cp -rT /etc/skel ~
touch ~/.init_done
fi
EOT
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}"
}
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
}
}
# --- Language installation ---
# All languages install in a single script to avoid apt-get lock
# conflicts (coder_script resources run in parallel).
resource "coder_script" "install_languages" {
count = length(local.languages) > 0 ? 1 : 0
agent_id = coder_agent.main.id
display_name = "Install Languages"
icon = "/icon/code.svg"
run_on_start = true
start_blocks_login = true
script = templatefile("${path.module}/install-languages.sh.tftpl", {
LANGUAGES = join(",", local.languages)
})
}
# --- IDE modules ---
module "code-server" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "code-server") ? 1 : 0)
source = "registry.coder.com/coder/code-server/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
order = 1
}
module "vscode-desktop" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "vscode-desktop") ? 1 : 0)
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
folder = "/home/coder"
order = 2
}
module "cursor" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "cursor") ? 1 : 0)
source = "registry.coder.com/coder/cursor/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
folder = "/home/coder"
order = 3
}
# TODO: Re-add the coder/jetbrains module once Coder's dynamic
# parameter system respects module count for parameter visibility.
# The module's internal coder_parameter appears even when count = 0,
# creating a ghost parameter in the workspace creation form.
# module "jetbrains" {
# count = data.coder_workspace.me.start_count * (contains(local.ides, "jetbrains") && length(local.jetbrains_selected) > 0 ? 1 : 0)
# source = "registry.coder.com/coder/jetbrains/coder"
# version = "~> 1.0"
# agent_id = coder_agent.main.id
# folder = "/home/coder"
# default = toset(local.jetbrains_selected)
# }
module "zed" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "zed") ? 1 : 0)
source = "registry.coder.com/coder/zed/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
folder = "/home/coder"
order = 5
}
module "windsurf" {
count = data.coder_workspace.me.start_count * (contains(local.ides, "windsurf") ? 1 : 0)
source = "registry.coder.com/coder/windsurf/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
folder = "/home/coder"
order = 6
}
# --- Git clone ---
module "git-clone" {
count = data.coder_workspace.me.start_count * (data.coder_parameter.git_repo.value != "" ? 1 : 0)
source = "registry.coder.com/coder/git-clone/coder"
version = "~> 1.0"
agent_id = coder_agent.main.id
url = data.coder_parameter.git_repo.value
}
# --- Presets ---
data "coder_workspace_preset" "web_dev" {
name = "Web Development"
icon = "/icon/nodejs.svg"
parameters = {
languages = jsonencode(["python", "nodejs"])
ides = jsonencode(["code-server"])
git_repo = ""
}
}
data "coder_workspace_preset" "backend_go" {
name = "Backend (Go)"
icon = "/icon/go.svg"
parameters = {
languages = jsonencode(["go"])
ides = jsonencode(["code-server", "jetbrains"])
jetbrains_ides = jsonencode(["GO"])
git_repo = ""
}
}
data "coder_workspace_preset" "data_science" {
name = "Data Science"
icon = "/icon/python.svg"
parameters = {
languages = jsonencode(["python"])
ides = jsonencode(["code-server"])
git_repo = ""
}
}
data "coder_workspace_preset" "full_stack" {
name = "Full Stack"
icon = "/icon/code.svg"
parameters = {
languages = jsonencode(["python", "nodejs", "go"])
ides = jsonencode(["code-server", "cursor"])
git_repo = ""
}
}
# --- Docker resources ---
resource "docker_volume" "home_volume" {
name = "coder-${data.coder_workspace.me.id}-home"
lifecycle {
ignore_changes = all
}
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_at_creation"
value = data.coder_workspace.me.name
}
depends_on = []
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = "codercom/enterprise-base:ubuntu"
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
hostname = data.coder_workspace.me.name
entrypoint = [
"sh", "-c",
replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
]
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/home/coder"
volume_name = docker_volume.home_volume.name
read_only = false
}
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
}
depends_on = []
}