Files
code-tools/terraform/workspace.tf
2025-09-29 14:14:30 +00:00

692 lines
22 KiB
HCL

locals {
workspace_agent_scripts = {
"workspace-setup.sh" = file("${path.module}/scripts/workspace-setup.sh")
"dev-tools.sh" = file("${path.module}/scripts/dev-tools.sh")
"terminal-tools.sh" = file("${path.module}/scripts/terminal-tools.sh")
"git-hooks.sh" = file("${path.module}/scripts/git-hooks.sh")
"marimo-setup.sh" = file("${path.module}/scripts/marimo-setup.sh")
"claude-install.sh" = file("${path.module}/scripts/claude-install.sh")
"codex-setup.sh" = file("${path.module}/scripts/codex-setup.sh")
"cursor-setup.sh" = file("${path.module}/scripts/cursor-setup.sh")
"windsurf-setup.sh" = file("${path.module}/scripts/windsurf-setup.sh")
}
}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
dir = "/workspaces"
env = merge(
{
"GIT_AUTHOR_NAME" = local.git_author_name
"GIT_AUTHOR_EMAIL" = local.git_author_email
"CODER_WORKSPACE_ID" = local.workspace_id
"CODER_WORKSPACE_REPO" = local.project_repo_url
"ENABLE_MARIMO" = tostring(local.marimo_enabled)
"ENABLE_SERVICES" = tostring(local.services_enabled)
"CODER_AGENT_BLOCK_FILE_TRANSFER" = var.block_file_transfer ? "1" : ""
"GEM_HOME" = "/home/coder/.gem"
"GEM_PATH" = "/home/coder/.gem"
"rvm_silence_path_mismatch_check_flag" = "1"
"RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG" = "1"
"rvmsudo_secure_path" = "1"
"RVMSUDO_SECURE_PATH" = "1"
"NVM_DIR" = "/usr/local/share/nvm"
},
local.gitea_pat != "" ? { "GITEA_PAT" = local.gitea_pat } : {},
local.github_pat != "" ? { "GITHUB_PAT" = local.github_pat } : {}
)
startup_script = <<-EOT
#!/bin/bash
set -euo pipefail
# Basic setup
${local.agent_startup}
# Ensure coder user can access Docker socket
if getent group docker >/dev/null 2>&1; then
if id -nG coder | tr ' ' '\n' | grep -qx docker; then
:
else
echo "Adding coder user to docker group..."
usermod -aG docker coder
fi
else
echo "docker group not found; skipping docker socket setup"
fi
# Switch to coder user for service startup with gem environment pre-set
sudo --preserve-env=CODER_WORKSPACE_ID,CODER_WORKSPACE_REPO,GITEA_PAT,GITHUB_PAT -u coder \
env -i \
HOME=/home/coder \
PATH="/home/coder/.venv/bin:/home/coder/.local/bin:/home/coder/bin:/home/coder/.cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin" \
GEM_HOME=/home/coder/.gem \
GEM_PATH=/home/coder/.gem \
NVM_DIR=/usr/local/share/nvm \
CODER_WORKSPACE_ID="$${CODER_WORKSPACE_ID-}" \
CODER_WORKSPACE_REPO="$${CODER_WORKSPACE_REPO-}" \
GITEA_PAT="$${GITEA_PAT-}" \
GITHUB_PAT="$${GITHUB_PAT-}" \
rvm_silence_path_mismatch_check_flag=1 \
RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1 \
rvmsudo_secure_path=1 \
RVMSUDO_SECURE_PATH=1 \
bash <<'CODER_SETUP'
set -euo pipefail
# Consistent RubyGems environment for every shell
export GEM_HOME="$HOME/.gem"
export GEM_PATH="$GEM_HOME"
mkdir -p "$GEM_HOME/bin"
case ":$PATH:" in
*":$GEM_HOME/bin:"*) ;;
*) export PATH="$GEM_HOME/bin:$PATH" ;;
esac
ensure_shell_env_block() {
local target="$1"
local start_marker="# >>> coder gem environment >>>"
local end_marker="# <<< coder gem environment <<<"
mkdir -p "$(dirname "$target")"
touch "$target"
if grep -q "$start_marker" "$target"; then
tmp_file=$(mktemp)
sed "/$start_marker/,/$end_marker/d" "$target" > "$tmp_file"
mv "$tmp_file" "$target"
fi
sed -i '/# RVM environment fix/,+3d' "$target" 2>/dev/null || true
cat <<'EOF' >> "$target"
# >>> coder gem environment >>>
export GEM_HOME="$HOME/.gem"
export GEM_PATH="$GEM_HOME"
export rvm_silence_path_mismatch_check_flag=1
export RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG=1
if [ -d "$HOME/.gem/bin" ]; then
case ":$PATH:" in
*":$HOME/.gem/bin:"*) ;;
*) export PATH="$HOME/.gem/bin:$PATH";;
esac
else
mkdir -p "$HOME/.gem/bin"
case ":$PATH:" in
*":$HOME/.gem/bin:"*) ;;
*) export PATH="$HOME/.gem/bin:$PATH";;
esac
fi
# <<< coder gem environment <<<
EOF
}
current_nvm=$(printenv NVM_DIR 2>/dev/null || true)
if [ -z "$current_nvm" ]; then
NVM_DIR=/usr/local/share/nvm
else
NVM_DIR="$current_nvm"
fi
export NVM_DIR
if [ -d "$NVM_DIR" ]; then
if [ -s "$NVM_DIR/nvm.sh" ]; then
# shellcheck disable=SC1090,SC1091
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1 || true
fi
shopt -s nullglob
for node_bin in "$NVM_DIR"/versions/node/*/bin; do
PATH="$node_bin:$PATH"
done
shopt -u nullglob
fi
for shell_rc in ~/.bashrc ~/.profile ~/.zprofile ~/.zshrc ~/.zshenv; do
ensure_shell_env_block "$shell_rc"
done
# Clone requested repository into /workspaces if not already present
mkdir -p /workspaces
workspace_has_content=false
if [ -n "$(find /workspaces -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'lost+found' -print -quit 2>/dev/null)" ]; then
workspace_has_content=true
fi
repo_value=$(printenv CODER_WORKSPACE_REPO 2>/dev/null || true)
repo_display="<empty>"
if [ -n "$repo_value" ]; then
repo_display="$repo_value"
fi
echo "CODER_WORKSPACE_REPO=$repo_display"
if [ -n "$repo_value" ]; then
if [ -d /workspaces/.git ]; then
echo "Git repository already present in /workspaces"
else
if [ "$workspace_has_content" = "false" ]; then
echo "Cloning $${CODER_WORKSPACE_REPO} into /workspaces"
if (cd /workspaces && git clone "$${CODER_WORKSPACE_REPO}" .); then
echo "Repository clone completed"
else
echo "Git clone failed; initializing empty repository"
if (cd /workspaces && git init .); then
echo "Initialized empty workspace directory"
else
echo "Failed to initialise repository; please check permissions"
fi
fi
else
echo "/workspaces already contains files; skipping automatic clone"
fi
fi
else
echo "No repository requested"
fi
cd "$HOME"
mkdir -p "$HOME/.config/fish/conf.d"
cat <<'EOF' > "$HOME/.config/fish/conf.d/gem_env.fish"
set -gx GEM_HOME $HOME/.gem
set -gx GEM_PATH $HOME/.gem
set -gx rvm_silence_path_mismatch_check_flag 1
set -gx RVM_SILENCE_PATH_MISMATCH_CHECK_FLAG 1
if not contains $HOME/.gem/bin $PATH
set -gx PATH $HOME/.gem/bin $PATH
end
EOF
# Materialize bundled scripts for tooling, workspace setup, and optional AI helpers
SCRIPT_ROOT="$HOME/.coder/scripts"
mkdir -p "$SCRIPT_ROOT"
install_embedded_script() {
local name="$1"
local encoded="$2"
if [ -z "$encoded" ]; then
echo "Skipping $name; embedded content missing"
return
fi
printf '%s' "$encoded" | base64 -d > "$SCRIPT_ROOT/$name"
chmod +x "$SCRIPT_ROOT/$name"
}
%{for script_name, script_content in local.workspace_agent_scripts}
install_embedded_script "${script_name}" "${base64encode(script_content)}"
%{endfor}
# Run bundled scripts after ensuring they exist on disk
run_script() {
local label="$1"
local path="$2"
if [ -x "$path" ]; then
echo "Running $label..."
"$path"
elif [ -f "$path" ]; then
echo "Running $label from $path..."
bash "$path"
else
echo "Skipping $label; script not found at $path"
fi
}
run_script "workspace setup" "$SCRIPT_ROOT/workspace-setup.sh"
run_script "developer tools check" "$SCRIPT_ROOT/dev-tools.sh"
run_script "terminal tools verifier" "$SCRIPT_ROOT/terminal-tools.sh"
run_script "git hooks configuration" "$SCRIPT_ROOT/git-hooks.sh"
if [ "${tostring(local.marimo_enabled)}" = "true" ]; then
run_script "Marimo setup" "$SCRIPT_ROOT/marimo-setup.sh"
else
echo "Marimo disabled (enable_marimo = ${tostring(local.marimo_enabled)})"
fi
if [ "${tostring(local.ai_enabled)}" = "true" ]; then
run_script "Claude CLI setup" "$SCRIPT_ROOT/claude-install.sh"
run_script "Codex CLI setup" "$SCRIPT_ROOT/codex-setup.sh"
run_script "Cursor setup" "$SCRIPT_ROOT/cursor-setup.sh"
run_script "Windsurf setup" "$SCRIPT_ROOT/windsurf-setup.sh"
else
echo "AI tooling disabled (enable_ai_tools = ${tostring(local.ai_enabled)})"
fi
echo "Services started successfully"
CODER_SETUP
echo "Agent startup completed"
EOT
metadata {
display_name = "CPU Usage"
key = "0_cpu_usage"
script = <<-EOT
# Get comprehensive CPU usage breakdown
container_cpu="n/a"
vm_cpu="n/a"
device_cores="n/a"
# Container CPU (if available)
if command -v docker >/dev/null 2>&1; then
container_cpu=$(docker stats --no-stream --format "{{.CPUPerc}}" $(hostname) 2>/dev/null || echo "n/a")
fi
# VM CPU from /proc/stat
vm_cpu=$(awk '/cpu /{u=$2+$4; t=$2+$3+$4+$5; if (NR==1){u1=u; t1=t;} else printf "%.1f%%", (u-u1) * 100 / (t-t1); }' \
<(grep 'cpu ' /proc/stat) <(sleep 1; grep 'cpu ' /proc/stat) 2>/dev/null || echo "n/a")
# Device cores
device_cores=$(nproc 2>/dev/null || echo "n/a")
# Format output on single line
if [ "$container_cpu" != "n/a" ]; then
echo "C:$container_cpu | Device:$${device_cores}c"
else
echo "VM:$vm_cpu | Device:$${device_cores}c"
fi
EOT
interval = 30
timeout = 15
}
metadata {
display_name = "Memory Usage"
key = "1_memory_usage"
script = <<-EOT
# Get comprehensive memory usage breakdown
container_mem="n/a"
vm_mem="n/a"
device_total="128GB"
device_vram="64GB"
# Container memory (if available)
if command -v docker >/dev/null 2>&1; then
container_mem=$(docker stats --no-stream --format "{{.MemUsage}}" $(hostname) 2>/dev/null || echo "n/a")
fi
# VM memory from /proc/meminfo (should show ~32GB allocated to VM)
vm_mem=$(awk '/^MemTotal:/{total=$2} /^MemAvailable:/{avail=$2} END{used=total-avail; printf "%.2f/%.2f GB", used/1024/1024, total/1024/1024}' /proc/meminfo 2>/dev/null || echo "n/a")
# Format output on single line
if [ "$container_mem" != "n/a" ]; then
echo "C:$container_mem | Device:$device_total ($device_vram VRAM)"
else
echo "VM:$vm_mem | Device:$device_total ($device_vram VRAM)"
fi
EOT
interval = 30
timeout = 15
}
metadata {
display_name = "Disk Usage"
key = "2_disk_usage"
script = <<-EOT
# Get comprehensive disk usage breakdown
workspace_disk="n/a"
home_disk="n/a"
root_disk="n/a"
# Workspace volume (/workspaces)
if [ -d "/workspaces" ]; then
workspace_disk=$(df -BG /workspaces 2>/dev/null | awk 'NR==2 {
used = $3; gsub(/G/, "", used);
total = $2; gsub(/G/, "", total);
printf "%dGB/%dGB", used, total
}' || echo "n/a")
fi
# Home volume (/home) - should be ~1TB
if [ -d "/home" ]; then
home_disk=$(df -BG /home 2>/dev/null | awk 'NR==2 {
used = $3; gsub(/G/, "", used);
total = $2; gsub(/G/, "", total);
printf "%dGB/%dTB", used, int(total/1000)
}' || echo "n/a")
fi
# Root filesystem (/) - should be ~98GB
root_disk=$(df -BG / 2>/dev/null | awk 'NR==2 {
used = $3; gsub(/G/, "", used);
total = $2; gsub(/G/, "", total);
printf "%dGB/%dGB", used, total
}' || echo "n/a")
# Format output on single line
if [ "$workspace_disk" != "n/a" ] && [ "$home_disk" != "n/a" ]; then
echo "W:$workspace_disk | H:$home_disk"
elif [ "$workspace_disk" != "n/a" ]; then
echo "W:$workspace_disk | Root:$root_disk"
elif [ "$home_disk" != "n/a" ]; then
echo "Root:$root_disk | H:$home_disk"
else
echo "Root:$root_disk"
fi
EOT
interval = 300
timeout = 15
}
metadata {
display_name = "Git Branch"
key = "3_git_branch"
script = <<-EOT
# Check for git repository dynamically
if [ -n "$CODER_WORKSPACE_REPO" ] && [ "$CODER_WORKSPACE_REPO" != "" ]; then
# Extract repo name from URL and look for it
repo_name=$(basename "$CODER_WORKSPACE_REPO" .git)
if [ -d "/workspaces/$repo_name/.git" ]; then
cd "/workspaces/$repo_name" && git branch --show-current 2>/dev/null || echo 'detached'
elif [ -d "/workspaces/.git" ]; then
cd "/workspaces" && git branch --show-current 2>/dev/null || echo 'detached'
else
echo 'no-repo'
fi
else
# Fallback to checking workspace root
cd /workspaces && git branch --show-current 2>/dev/null || echo 'no-repo'
fi
EOT
interval = 300
timeout = 5
}
metadata {
display_name = "PostgreSQL"
key = "4_postgres"
script = local.services_enabled ? format("pg_isready -h postgres-%s -p 5432 -U postgres >/dev/null && echo healthy || echo down", local.workspace_id) : "echo 'disabled'"
interval = 60
timeout = 5
}
metadata {
display_name = "Redis"
key = "5_redis"
script = local.services_enabled ? format("redis-cli -h redis-%s -a %s ping 2>/dev/null | grep -qi pong && echo healthy || echo down", local.workspace_id, var.redis_password) : "echo 'disabled'"
interval = 60
timeout = 5
}
metadata {
display_name = "Qdrant"
key = "6_qdrant"
script = <<-EOT
if [ "${tostring(local.services_enabled)}" = "true" ]; then
target="qdrant-${local.workspace_id}"
if command -v curl >/dev/null 2>&1; then
status=$(curl -s -o /dev/null -w "%%{http_code}" --connect-timeout 3 "http://$target:6333/healthz")
if [ "$status" = "200" ]; then echo healthy; else echo "unhealthy($status)"; fi
elif command -v wget >/dev/null 2>&1; then
if wget -qO- --timeout=3 "http://$target:6333/healthz" >/dev/null; then echo healthy; else echo down; fi
else
echo unknown
fi
else
echo 'disabled'
fi
EOT
interval = 60
timeout = 5
}
}
resource "docker_container" "workspace" {
count = data.coder_workspace.me.start_count
image = docker_image.devcontainer.image_id
name = local.container_name
hostname = data.coder_workspace.me.name
memory = var.workspace_memory_limit > 0 ? var.workspace_memory_limit * 1024 * 1024 : null
# Enable privileged mode for full system access
privileged = true
# Run as root to avoid permission issues
user = "root"
env = compact([
"GIT_AUTHOR_NAME=${local.git_author_name}",
"GIT_AUTHOR_EMAIL=${local.git_author_email}",
"CODER_AGENT_TOKEN=${coder_agent.main.token}",
"CODER_AGENT_DEVCONTAINERS_ENABLE=true",
local.project_repo_url != "" ? "CODER_WORKSPACE_REPO=${local.project_repo_url}" : "",
"HOME=/home/coder",
"USER=coder",
])
networks_advanced {
name = docker_network.workspace.name
}
host {
host = "host.docker.internal"
ip = "host-gateway"
}
volumes {
container_path = "/workspaces"
volume_name = docker_volume.workspaces.name
}
dynamic "volumes" {
for_each = local.bind_mounts
content {
host_path = volumes.value.host
container_path = volumes.value.container
read_only = try(volumes.value.read_only, false)
}
}
dynamic "volumes" {
for_each = var.enable_docker_in_docker ? [1] : []
content {
host_path = "/var/run/docker.sock"
container_path = "/var/run/docker.sock"
}
}
working_dir = "/workspaces"
command = ["/bin/bash", "-c", <<-EOT
# Run init script as root to handle permissions
${coder_agent.main.init_script}
# Switch to coder user for ongoing operations
echo "Switching to coder user for services..."
exec sudo -u coder -i bash -c 'cd /workspaces && sleep infinity'
EOT
]
labels {
label = "coder.owner"
value = data.coder_workspace_owner.me.name
}
labels {
label = "coder.workspace_id"
value = local.workspace_id
}
depends_on = [
docker_network.workspace,
docker_volume.workspaces,
docker_image.devcontainer
]
}
module "code_server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.1"
agent_id = coder_agent.main.id
install_prefix = "/opt/code-server"
offline = true
port = 13337
folder = "/workspaces"
display_name = "code-server"
group = "Web IDEs"
order = 10
}
module "cursor_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/cursor/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
group = "Desktop IDEs"
order = 40
}
module "windsurf_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
source = "registry.coder.com/coder/windsurf/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
group = "Desktop IDEs"
order = 50
}
module "pycharm_desktop" {
count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_jetbrains.value ? 1 : 0
source = "registry.coder.com/coder/jetbrains-gateway/coder"
agent_id = coder_agent.main.id
folder = "/workspaces"
jetbrains_ides = ["PY"]
default = "PY"
group = "Desktop IDEs"
order = 60
coder_parameter_order = 6
slug = "pycharm-gateway"
}
# module "claude_code" {
# count = data.coder_workspace.me.start_count > 0 && data.coder_parameter.enable_ai_tools.value ? 1 : 0
# source = "registry.coder.com/coder/claude-code/coder"
# agent_id = coder_agent.main.id
# workdir = "/workspaces"
# group = "AI Tools"
# order = 30
# install_claude_code = false
# install_agentapi = true
# pre_install_script = <<-EOT
# #!/usr/bin/env bash
# set -euo pipefail
#
# # We're running as root, so set up the coder user properly
# target_user="coder"
# target_home="/home/coder"
#
# # Ensure coder user exists
# if ! id "$target_user" >/dev/null 2>&1; then
# useradd -m -s /bin/bash "$target_user" || true
# fi
#
# # Create home directory if it doesn't exist
# mkdir -p "$target_home"
# chown "$target_user:$target_user" "$target_home"
#
# # Setup passwordless sudo for coder user
# echo "$target_user ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$target_user"
# chmod 440 "/etc/sudoers.d/$target_user"
# echo "Passwordless sudo configured for $target_user"
#
# # Ensure proper ownership of important directories
# mkdir -p "$target_home/.local/bin"
# chown -R "$target_user:$target_user" "$target_home/.local"
#
# real_curl="$(command -v curl || true)"
# wrapper=/usr/local/bin/curl
# if [ -n "$real_curl" ]; then
# if ! curl --help 2>/dev/null | grep -q -- "--retry-all-errors"; then
# real_curl="$(readlink -f "$real_curl" 2>/dev/null || echo "$real_curl")"
# if [ "$real_curl" != "$wrapper" ]; then
# python3 - <<'PY' "$real_curl" "$wrapper"
# import os, sys, stat
# real=sys.argv[1]; wrapper=sys.argv[2]
# code=f"""#!/usr/bin/env python3
# import os, sys
# real={real!r}
# args=[a for a in sys.argv[1:] if a != "--retry-all-errors"]
# os.execv(real,[real]+args)
# """
# with open(wrapper,'w', encoding='utf-8') as f:
# f.write(code)
# os.chmod(wrapper, 0o755)
# PY
# fi
# fi
# fi
#
# agentapi_version="v0.7.1"
# bin_dir="$HOME/.local/bin"
# mkdir -p "$bin_dir"
#
# # Ensure agentapi scripts directory exists
# agentapi_scripts_dir="$HOME/code-tools/tf/scripts/agentapi"
# if [ ! -d "$agentapi_scripts_dir" ]; then
# echo "Creating agentapi scripts directory: $agentapi_scripts_dir"
# mkdir -p "$agentapi_scripts_dir"
# # Create minimal placeholder scripts if they don't exist
# if [ ! -f "$agentapi_scripts_dir/agentapi-start.sh" ]; then
# cat > "$agentapi_scripts_dir/agentapi-start.sh" << 'SCRIPT_EOF'
# #!/bin/bash
# echo "AgentAPI start script placeholder"
# SCRIPT_EOF
# chmod +x "$agentapi_scripts_dir/agentapi-start.sh"
# fi
# fi
#
# ensure_agentapi() {
# if command -v agentapi >/dev/null 2>&1; then
# return 0
# fi
#
# arch=$(uname -m)
# case "$arch" in
# x86_64|amd64)
# asset="agentapi-linux-amd64"
# ;;
# aarch64|arm64)
# asset="agentapi-linux-arm64"
# ;;
# *)
# echo "warning: unsupported architecture $arch; skipping agentapi bootstrap" >&2
# return 1
# ;;
# esac
#
# if [ "$agentapi_version" = "latest" ]; then
# url="https://github.com/coder/agentapi/releases/latest/download/$${asset}"
# else
# url="https://github.com/coder/agentapi/releases/download/$${agentapi_version}/$${asset}"
# fi
#
# tmp_file=$(mktemp)
# if ! curl -fsSL "$url" -o "$tmp_file"; then
# echo "warning: failed to download agentapi from $url" >&2
# rm -f "$tmp_file"
# return 1
# fi
#
# if command -v install >/dev/null 2>&1; then
# install -m 0755 "$tmp_file" "$bin_dir/agentapi"
# else
# mv "$tmp_file" "$bin_dir/agentapi"
# chmod 0755 "$bin_dir/agentapi"
# fi
# rm -f "$tmp_file"
# echo "Installed agentapi CLI into $bin_dir/agentapi"
# }
#
# ensure_agentapi
#
# if ! command -v claude >/dev/null 2>&1; then
# echo "warning: claude CLI not found; expected pre-installed in base image." >&2
# fi
#
# case ":$PATH:" in
# *:"$bin_dir":*) ;;
# *) echo "PATH does not include $bin_dir; adding for current session." >&2; export PATH="$bin_dir:$PATH" ;;
# esac
# EOT
# }