Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions coderd/templatebuilder/bases.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ type BaseManifest struct {
OS string `json:"os"`
DefaultContext BaseDefaultContext `json:"default_context"`
Variables []ModuleVariable `json:"variables"`
// IncludedModules lists the catalog module IDs this base already
// declares in its own Terraform (e.g. "git-clone"). Compose treats
// these names as occupied so a wizard-selected module cannot collide
// with one the base already renders.
IncludedModules []string `json:"included_modules,omitempty"`
}

// BaseDefaultContext holds default render values stored in base.json.
Expand Down Expand Up @@ -260,6 +265,17 @@ func BaseVariables(exampleID string) []ModuleVariable {
return bases[exampleID].Manifest.Variables
}

// BaseIncludedModules returns the catalog module IDs the given base declares
// in its own Terraform (see BaseManifest.IncludedModules). Returns nil if the
// base is unknown or declares none.
func BaseIncludedModules(exampleID string) []string {
bases, err := loadBases()
if err != nil || bases[exampleID] == nil {
return nil
}
return bases[exampleID].Manifest.IncludedModules
}

// BaseTemplateFS returns a filesystem rooted at the given base template
// directory within the embedded bases catalog. Returns an error if
// exampleID is not a known base template.
Expand Down
64 changes: 64 additions & 0 deletions coderd/templatebuilder/bases/quickstart/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
display_name: Coder Quickstart
description: Get started with Coder by picking your languages and a repo
icon: ../../../site/static/icon/coder.svg
maintainer_github: coder
verified: true
tags: [docker, quickstart]
---

# Coder Quickstart

Get up and running with Coder in minutes. Choose your programming languages, optionally clone a Git repository, and start coding.

## How It Works

When you create a workspace from this template, you select:

1. **Languages** to pre-install (Python, Node.js, Go, Rust, Java, C/C++)
2. **A Git repository** to clone (optional)

Coder provisions a workspace with your selections and you can start developing immediately.

<!-- prerequisites:start -->

## Prerequisites

The host running Coder must have a Docker daemon accessible to the `coder` user:

```sh
# Add coder user to Docker group
sudo adduser coder docker

# Restart Coder server
sudo systemctl restart coder

# Verify access
sudo -u coder docker ps
```

<!-- prerequisites:end -->

## Architecture

This template provisions:

- **Docker container** (ephemeral) running Ubuntu with the Coder agent
- **Docker volume** (persistent) mounted at `/home/coder`

Files in your home directory (`/home/coder`) persist across workspace restarts. The language install script runs on every start and blocks login until it finishes. Most toolchains install into the ephemeral workspace container rather than your home directory, so they are reinstalled from the network on each start; the exception is Rust, whose toolchain lives under `~/.cargo` in your home directory and is detected and reused.

## Presets

Select a preset to auto-fill languages for common workflows:

| Preset | Languages |
| ------------------- | ------------------- |
| **Web Development** | Python, Node.js |
| **Backend (Go)** | Go |
| **Data Science** | Python |
| **Full Stack** | Python, Node.js, Go |

## Editors

VS Code Desktop is available on every workspace by default (Coder enables the VS Code Desktop display app automatically). To add more editors (VS Code in the browser, Cursor, JetBrains, Zed, Windsurf) or other tools, add them as modules in the next step of the template builder.
7 changes: 7 additions & 0 deletions coderd/templatebuilder/bases/quickstart/base.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "quickstart",
"display_name": "Coder Quickstart",
"os": "linux",
"default_context": {},
"included_modules": ["git-clone"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-12] The collision guard's invariant is not enforced: included_modules is a hand-maintained duplicate of the base's actual module blocks, and nothing binds them (Hisoka, Mafuuu, Pariston, Chopper, Melody, Meruem, Zoro P3; Knov P2).

Today included_modules: ["git-clone"] matches the single module "git-clone" block. But the two live in separate files with no link, and no test cross-checks them (the only reference is BaseExcludesIncludedModules, which asserts the endpoint honors the JSON, not that the JSON describes reality). If a future edit adds a catalog-named module block and forgets the JSON line (or renames the block), validateModules stops reserving the name and the endpoint starts offering it, and CRF-4's P1 duplicate-module break returns with no failing test. There is also no check that included_modules entries are real catalog IDs, so a typo silently protects the wrong name.

The R4 fix closed the git-clone instance; this closes the class. Mechanical fix (AGENTS.md prefers this): a test that renders each base, scans for module "<id>" labels that are catalog IDs, and asserts the set equals that base's included_modules, or derive the set from the rendered base (the package already parses base HCL in ExtractAgentResourceName) and delete the field.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enforced in 657abbb. Added ExtractModuleNames and TestBaseIncludedModulesMatchRendered: it renders each base, keeps the module "<id>" labels that are catalog IDs, and asserts that set equals the base's included_modules. A base that adds a catalog-named module block without the manifest line (or lists a phantom/typo'd one) now fails. Verified empirically: it fails when I temporarily emptied quickstart's included_modules, and passes once restored.

}
98 changes: 98 additions & 0 deletions coderd/templatebuilder/bases/quickstart/install-languages.sh.tftpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
set -e

LANGUAGES="${LANGUAGES}"
APT_UPDATED=false

apt_update() {
if [ "$APT_UPDATED" = "false" ]; then
sudo apt-get update -qq
APT_UPDATED=true
fi
}

# has_language reports whether NAME is one of the selected languages. It matches
# whole comma-separated entries, so a value can never partially match another
# (guards against a future language whose name contains an existing one).
has_language() {
case ",$LANGUAGES," in
*",$1,"*) return 0 ;;
*) return 1 ;;
esac
}

if has_language python; then
if command -v python3 >/dev/null 2>&1; then
echo "Python: $(python3 --version)"
else
echo "Installing Python..."
apt_update
sudo apt-get install -y -qq python3 python3-pip python3-venv
echo "Installed Python: $(python3 --version)"
fi
fi

if has_language nodejs; then
if command -v node >/dev/null 2>&1; then
echo "Node.js: $(node --version)"
else
echo "Installing Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y -qq nodejs
echo "Installed Node.js: $(node --version)"
fi
fi

if has_language go; then
if command -v /usr/local/go/bin/go >/dev/null 2>&1; then
echo "Go: $(/usr/local/go/bin/go version)"
else
echo "Installing Go..."
ARCH=$(uname -m)
case $ARCH in
x86_64) GOARCH="amd64" ;;
aarch64) GOARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
esac
GO_VERSION=$(curl -fsSL "https://go.dev/VERSION?m=text" | head -1)
curl -fsSL "https://go.dev/dl/$${GO_VERSION}.linux-$${GOARCH}.tar.gz" | sudo tar -C /usr/local -xz
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' | sudo tee /etc/profile.d/go.sh >/dev/null
echo "Installed Go: $(/usr/local/go/bin/go version)"
fi
fi

if has_language rust; then
if command -v rustc >/dev/null 2>&1 || [ -f "$HOME/.cargo/bin/rustc" ]; then
RUSTC=$${HOME}/.cargo/bin/rustc
command -v rustc >/dev/null 2>&1 && RUSTC=rustc
echo "Rust: $($RUSTC --version)"
else
echo "Installing Rust..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "Installed Rust: $($HOME/.cargo/bin/rustc --version)"
fi
fi

if has_language java; then
if command -v java >/dev/null 2>&1; then
echo "Java: $(java --version 2>&1 | head -1)"
else
echo "Installing Java (OpenJDK 21)..."
apt_update
sudo apt-get install -y -qq openjdk-21-jdk
echo "Installed Java: $(java --version 2>&1 | head -1)"
fi
fi

if has_language cpp; then
if command -v gcc >/dev/null 2>&1; then
echo "C/C++: $(gcc --version | head -1)"
else
echo "Installing C/C++ toolchain..."
apt_update
sudo apt-get install -y -qq gcc g++ make cmake
echo "Installed C/C++: $(gcc --version | head -1)"
fi
fi

echo "Language setup complete."
Loading
Loading