mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
363 lines
11 KiB
Terraform
363 lines
11 KiB
Terraform
terraform {
|
|
required_providers {
|
|
coder = {
|
|
source = "coder/coder"
|
|
version = "~>2"
|
|
}
|
|
incus = {
|
|
source = "lxc/incus"
|
|
version = "~>1.0"
|
|
}
|
|
}
|
|
}
|
|
|
|
data "coder_provisioner" "me" {}
|
|
|
|
provider "incus" {}
|
|
|
|
data "coder_workspace" "me" {}
|
|
data "coder_workspace_owner" "me" {}
|
|
|
|
data "coder_parameter" "image" {
|
|
name = "image"
|
|
display_name = "Image"
|
|
description = "The container image to use. Must have cloud-init installed."
|
|
default = "images:debian/13/cloud"
|
|
icon = "/icon/image.svg"
|
|
mutable = false
|
|
|
|
option {
|
|
name = "Debian 13 (Trixie)"
|
|
value = "images:debian/13/cloud"
|
|
}
|
|
option {
|
|
name = "Debian 12 (Bookworm)"
|
|
value = "images:debian/12/cloud"
|
|
}
|
|
option {
|
|
name = "Ubuntu 24.04 (Noble)"
|
|
value = "images:ubuntu/24.04/cloud"
|
|
}
|
|
option {
|
|
name = "Ubuntu 22.04 (Jammy)"
|
|
value = "images:ubuntu/22.04/cloud"
|
|
}
|
|
|
|
}
|
|
|
|
data "coder_parameter" "cpu" {
|
|
name = "cpu"
|
|
display_name = "CPU"
|
|
description = "The number of CPUs to allocate to the workspace (1-8)"
|
|
type = "number"
|
|
default = "1"
|
|
icon = "https://raw.githubusercontent.com/matifali/logos/main/cpu-3.svg"
|
|
mutable = true
|
|
validation {
|
|
min = 1
|
|
max = 8
|
|
}
|
|
}
|
|
|
|
data "coder_parameter" "memory" {
|
|
name = "memory"
|
|
display_name = "Memory"
|
|
description = "The amount of memory to allocate to the workspace in GB (up to 16GB)"
|
|
type = "number"
|
|
default = "2"
|
|
icon = "/icon/memory.svg"
|
|
mutable = true
|
|
validation {
|
|
min = 1
|
|
max = 16
|
|
}
|
|
}
|
|
|
|
data "coder_parameter" "git_repo" {
|
|
type = "string"
|
|
name = "Git repository"
|
|
default = ""
|
|
description = "Clone a git repo inside the workspace"
|
|
mutable = true
|
|
}
|
|
|
|
data "coder_parameter" "pool" {
|
|
type = "string"
|
|
name = "pool"
|
|
display_name = "Storage pool"
|
|
default = "coder"
|
|
description = "Incus storage pool name"
|
|
mutable = false
|
|
}
|
|
|
|
resource "coder_agent" "main" {
|
|
count = data.coder_workspace.me.start_count
|
|
arch = data.coder_provisioner.me.arch
|
|
os = "linux"
|
|
dir = "/home/${local.workspace_user}"
|
|
env = {
|
|
CODER_WORKSPACE_ID = data.coder_workspace.me.id
|
|
CODER_SESSION_TOKEN = data.coder_workspace_owner.me.session_token
|
|
CODER_URL = data.coder_workspace.me.access_url
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
# Note: execution order is currently not guaranteed so only
|
|
# include packages here that are not required for either the
|
|
# agent or modules.
|
|
resource "coder_script" "packages" {
|
|
count = data.coder_workspace.me.start_count
|
|
agent_id = coder_agent.main[0].id
|
|
display_name = "Install packages"
|
|
icon = "/icon/debian.svg"
|
|
run_on_start = true
|
|
script = <<-EOF
|
|
#!/bin/bash
|
|
set -e
|
|
PACKAGES=(docker.io)
|
|
MISSING=()
|
|
for pkg in "$${PACKAGES[@]}"; do
|
|
if ! dpkg -s "$pkg" &> /dev/null; then
|
|
MISSING+=("$pkg")
|
|
fi
|
|
done
|
|
if [ "$${#MISSING[@]}" -gt 0 ]; then
|
|
echo "Installing: $${MISSING[*]}"
|
|
sudo apt-get update
|
|
sudo apt-get install -y "$${MISSING[@]}"
|
|
|
|
echo "Packages installed successfully"
|
|
else
|
|
echo "All packages already installed"
|
|
fi
|
|
# Ensure the workspace user can access the Docker socket without
|
|
# needing the docker group (which would require a new login session).
|
|
if [ -S /var/run/docker.sock ]; then
|
|
sudo chown $(whoami) /var/run/docker.sock
|
|
fi
|
|
EOF
|
|
}
|
|
|
|
resource "incus_storage_volume" "home" {
|
|
name = "coder-${data.coder_workspace.me.id}-home"
|
|
pool = local.pool
|
|
}
|
|
|
|
resource "incus_instance" "dev" {
|
|
running = data.coder_workspace.me.start_count == 1
|
|
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
|
image = data.coder_parameter.image.value
|
|
|
|
config = {
|
|
"limits.cpu" = data.coder_parameter.cpu.value
|
|
"limits.memory" = "${data.coder_parameter.memory.value}GiB"
|
|
"security.nesting" = true
|
|
"security.syscalls.intercept.mknod" = true
|
|
"security.syscalls.intercept.setxattr" = true
|
|
"boot.autostart" = true
|
|
|
|
# Pass the agent token and URL via Incus user config keys.
|
|
# These are readable from inside the container via the guest API at
|
|
# /dev/incus/sock, which removes the need for bind-mounting files from
|
|
# the host. This decouples the provisioner from the Incus host. They
|
|
# no longer need to share a filesystem. The token is refreshed by
|
|
# Terraform on every workspace start; Incus updates the config on the
|
|
# instance even while it is stopped, so the new value is available
|
|
# immediately when the container boots.
|
|
"user.coder_agent_token" = local.agent_token
|
|
"user.coder_agent_url" = data.coder_workspace.me.access_url
|
|
|
|
"cloud-init.user-data" = <<EOF
|
|
#cloud-config
|
|
hostname: ${lower(data.coder_workspace.me.name)}
|
|
users:
|
|
- name: ${local.workspace_user}
|
|
uid: 1000
|
|
gid: 1000
|
|
groups: sudo
|
|
shell: /bin/bash
|
|
sudo: ['ALL=(ALL) NOPASSWD:ALL']
|
|
write_files:
|
|
# Pre-start script that reads the agent token and URL from the Incus guest
|
|
# API (/dev/incus/sock) and writes them to an env file. This runs as a
|
|
# separate oneshot unit (coder-agent-config.service) before the agent starts,
|
|
# ensuring the env file is always fresh. This approach:
|
|
# - Eliminates host filesystem coupling (no bind mounts for token or binary)
|
|
# - Allows the provisioner to run on a different host than Incus
|
|
# - Delivers a fresh agent token on every start without cloud-init re-runs
|
|
- path: /opt/coder/fetch-config.sh
|
|
permissions: "0755"
|
|
content: |
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
INCUS_SOCK="/dev/incus/sock"
|
|
|
|
# Read agent config from Incus guest API.
|
|
CODER_AGENT_TOKEN=$(curl -sf --unix-socket "$INCUS_SOCK" http://localhost/1.0/config/user.coder_agent_token)
|
|
CODER_AGENT_URL=$(curl -sf --unix-socket "$INCUS_SOCK" http://localhost/1.0/config/user.coder_agent_url)
|
|
|
|
# Write env file for the systemd service.
|
|
printf 'CODER_AGENT_TOKEN=%s\nCODER_AGENT_URL=%s\n' "$CODER_AGENT_TOKEN" "$CODER_AGENT_URL" > /opt/coder/init.env
|
|
# The standard Coder agent init script, provided by coder_agent.init_script.
|
|
# This handles downloading the correct agent binary and running it.
|
|
- path: /opt/coder/coder-init.sh
|
|
permissions: "0755"
|
|
encoding: b64
|
|
content: ${base64encode(local.agent_init_script)}
|
|
- path: /etc/systemd/system/coder-agent-config.service
|
|
permissions: "0644"
|
|
content: |
|
|
[Unit]
|
|
Description=Fetch Coder Agent Config from Incus Guest API
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/opt/coder/fetch-config.sh
|
|
# Watcher script that listens for config changes via the Incus guest API
|
|
# events endpoint. The Incus Terraform provider starts the instance before
|
|
# updating config keys, so on a stop->start cycle the agent initially boots
|
|
# with a stale token. This watcher detects when user.coder_agent_token is
|
|
# updated, re-fetches the config, and restarts the agent with the new token.
|
|
- path: /opt/coder/watch-config.sh
|
|
permissions: "0755"
|
|
content: |
|
|
#!/bin/bash
|
|
INCUS_SOCK="/dev/incus/sock"
|
|
curl -sfN --unix-socket "$INCUS_SOCK" http://localhost/1.0/events?type=config | \
|
|
while read -r event; do
|
|
key=$(echo "$event" | sed -n 's/.*"key":"\([^"]*\)".*/\1/p')
|
|
if [ "$key" = "user.coder_agent_token" ]; then
|
|
/opt/coder/fetch-config.sh
|
|
systemctl restart coder-agent.service
|
|
fi
|
|
done
|
|
- path: /etc/systemd/system/coder-agent-watcher.service
|
|
permissions: "0644"
|
|
content: |
|
|
[Unit]
|
|
Description=Watch for Coder Agent config changes via Incus Guest API
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
ExecStart=/opt/coder/watch-config.sh
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
- path: /etc/systemd/system/coder-agent.service
|
|
permissions: "0644"
|
|
content: |
|
|
[Unit]
|
|
Description=Coder Agent
|
|
After=network-online.target coder-agent-config.service
|
|
Wants=network-online.target
|
|
Requires=coder-agent-config.service
|
|
|
|
[Service]
|
|
User=${local.workspace_user}
|
|
EnvironmentFile=/opt/coder/init.env
|
|
ExecStart=/opt/coder/coder-init.sh
|
|
Restart=always
|
|
RestartSec=10
|
|
TimeoutStopSec=90
|
|
KillMode=process
|
|
OOMScoreAdjust=-900
|
|
SyslogIdentifier=coder-agent
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
runcmd:
|
|
- chown -R ${local.workspace_user}:${local.workspace_user} /home/${local.workspace_user}
|
|
# Install package dependencies before starting the agent.
|
|
- apt-get update && apt-get install -y curl git
|
|
- systemctl daemon-reload
|
|
- systemctl enable coder-agent.service coder-agent-watcher.service
|
|
- systemctl start coder-agent.service coder-agent-watcher.service
|
|
EOF
|
|
}
|
|
|
|
device {
|
|
name = "home"
|
|
type = "disk"
|
|
properties = {
|
|
path = "/home/${local.workspace_user}"
|
|
pool = local.pool
|
|
source = incus_storage_volume.home.name
|
|
}
|
|
}
|
|
|
|
device {
|
|
name = "root"
|
|
type = "disk"
|
|
properties = {
|
|
path = "/"
|
|
pool = local.pool
|
|
}
|
|
}
|
|
}
|
|
|
|
locals {
|
|
workspace_user = lower(data.coder_workspace_owner.me.name)
|
|
pool = data.coder_parameter.pool.value
|
|
# Workaround for the LXC provider stripping empty string config values, causing unexpected new values.
|
|
agent_token = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].token : "no-token"
|
|
agent_init_script = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].init_script : "#!/bin/sh\nexit 0"
|
|
}
|
|
|
|
resource "coder_metadata" "info" {
|
|
count = data.coder_workspace.me.start_count
|
|
resource_id = coder_agent.main[0].id
|
|
item {
|
|
key = "memory"
|
|
value = incus_instance.dev.config["limits.memory"]
|
|
}
|
|
item {
|
|
key = "cpus"
|
|
value = incus_instance.dev.config["limits.cpu"]
|
|
}
|
|
item {
|
|
key = "instance"
|
|
value = incus_instance.dev.name
|
|
}
|
|
item {
|
|
key = "image"
|
|
value = data.coder_parameter.image.value
|
|
}
|
|
}
|
|
|
|
module "code-server" {
|
|
source = "registry.coder.com/coder/code-server/coder"
|
|
version = "~> 1.0"
|
|
agent_id = coder_agent.main[0].id
|
|
count = data.coder_workspace.me.start_count
|
|
}
|
|
|
|
module "git-clone" {
|
|
count = data.coder_workspace.me.start_count == 1 && data.coder_parameter.git_repo.value != "" ? 1 : 0
|
|
source = "registry.coder.com/coder/git-clone/coder"
|
|
version = "~> 2.0"
|
|
agent_id = coder_agent.main[0].id
|
|
url = data.coder_parameter.git_repo.value
|
|
}
|