# ============================================================================= # Cloudflare Worker Development - Coder Template # ============================================================================= # Lightweight workspace for Cloudflare Workers with Hono, Drizzle, and # Claude Code automation. Supports automated SDLC via n8n triggers. # ============================================================================= terraform { required_providers { coder = { source = "coder/coder" } kubernetes = { source = "hashicorp/kubernetes" } } } # ============================================================================= # Data Sources # ============================================================================= data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} data "coder_provisioner" "me" {} data "coder_external_auth" "github" { id = "primary-github" } # ============================================================================= # Parameters # ============================================================================= data "coder_parameter" "cpu" { name = "cpu" display_name = "CPU Cores" description = "Number of CPU cores" type = "number" default = "2" mutable = false option { name = "1 Core" value = "1" } option { name = "2 Cores" value = "2" } option { name = "4 Cores" value = "4" } } data "coder_parameter" "memory" { name = "memory" display_name = "Memory (MB)" description = "Amount of memory" type = "number" default = "4096" mutable = false option { name = "2048 MB" value = "2048" } option { name = "4096 MB" value = "4096" } option { name = "8192 MB" value = "8192" } } # ─── Automation Parameters ─────────────────────────────────────────────────── data "coder_parameter" "repo_clone_url" { name = "repo_clone_url" display_name = "Repository Clone URL" description = "Git repo URL to clone (leave empty for manual setup)" type = "string" default = "" mutable = false } data "coder_parameter" "task_type" { name = "task_type" display_name = "Task Type" description = "Claude Code command to run automatically (leave empty for interactive use)" type = "string" default = "" mutable = false option { name = "None (interactive)" value = "" } option { name = "Analyse" value = "analyse" } option { name = "Architect" value = "architect" } option { name = "Develop" value = "develop" } option { name = "Review Code" value = "review-code" } option { name = "Test" value = "test" } option { name = "Fix Bug" value = "fix-bug" } option { name = "Rework PR" value = "rework-pr" } option { name = "Release" value = "release" } option { name = "DevOps" value = "devops" } option { name = "Create Migration" value = "create-migration" } } data "coder_parameter" "issue_number" { name = "issue_number" display_name = "Issue/PR Number" description = "Gitea issue or PR number (for automated tasks)" type = "string" default = "" mutable = false } data "coder_parameter" "gitea_token" { name = "gitea_token" display_name = "Gitea API Token" description = "API token for reading issues and posting comments" type = "string" default = "" mutable = false } data "coder_parameter" "gitea_org" { name = "gitea_org" display_name = "Gitea Organization" description = "Gitea organization name" type = "string" default = "claude" mutable = false } data "coder_parameter" "gitea_repo" { name = "gitea_repo" display_name = "Gitea Repository" description = "Gitea repository name" type = "string" default = "" mutable = false } data "coder_parameter" "anthropic_api_key" { name = "anthropic_api_key" display_name = "Anthropic API Key" description = "API key for Claude Code" type = "string" default = "" mutable = false } data "coder_parameter" "callback_url" { name = "callback_url" display_name = "Task Complete Callback URL" description = "URL to POST when task finishes (set by orchestrator)" type = "string" default = "" mutable = false } data "coder_parameter" "claude_oauth_token" { name = "claude_oauth_token" display_name = "Claude OAuth Token" description = "OAuth token for Claude Code Max subscription" type = "string" default = "" mutable = false } data "coder_parameter" "deploy_env" { name = "deploy_env" display_name = "Deploy Environment" description = "Target environment for deploy tasks" type = "string" default = "" mutable = false option { name = "None" value = "" } option { name = "Staging" value = "staging" } option { name = "Production" value = "production" } } # ============================================================================= # Locals # ============================================================================= locals { namespace = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" labels = { "app.kubernetes.io/name" = "coder-workspace" "app.kubernetes.io/instance" = data.coder_workspace.me.name "app.kubernetes.io/managed-by" = "coder" "coder.workspace.owner" = data.coder_workspace_owner.me.name } } # ============================================================================= # Namespace # ============================================================================= resource "kubernetes_namespace_v1" "workspace" { metadata { name = local.namespace labels = local.labels annotations = { "field.cattle.io/projectId" = "local:p-w2w48" } } } # ============================================================================= # Secrets - Registry Access # ============================================================================= data "kubernetes_secret_v1" "registry_source" { metadata { name = "registry-samson-media" namespace = "registry" } } resource "kubernetes_secret_v1" "registry" { metadata { name = "registry-samson-media" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } type = "kubernetes.io/dockerconfigjson" data = { ".dockerconfigjson" = data.kubernetes_secret_v1.registry_source.data[".dockerconfigjson"] } } # ============================================================================= # RBAC # ============================================================================= resource "kubernetes_service_account_v1" "workspace" { metadata { name = "workspace" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } } resource "kubernetes_role_v1" "workspace" { metadata { name = "workspace-admin" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } rule { api_groups = ["", "apps", "batch", "networking.k8s.io"] resources = ["*"] verbs = ["*"] } } resource "kubernetes_role_binding_v1" "workspace" { metadata { name = "workspace-admin" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } role_ref { api_group = "rbac.authorization.k8s.io" kind = "Role" name = kubernetes_role_v1.workspace.metadata[0].name } subject { kind = "ServiceAccount" name = kubernetes_service_account_v1.workspace.metadata[0].name namespace = kubernetes_namespace_v1.workspace.metadata[0].name } } resource "kubernetes_role_binding_v1" "rancher_user_access" { metadata { name = "rancher-user-access" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } role_ref { api_group = "rbac.authorization.k8s.io" kind = "ClusterRole" name = "coder-developer" } subject { api_group = "rbac.authorization.k8s.io" kind = "User" name = data.coder_workspace_owner.me.name } } # ============================================================================= # Resource Quotas # ============================================================================= resource "kubernetes_resource_quota_v1" "workspace" { metadata { name = "workspace-quota" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } spec { hard = { "requests.cpu" = "${tonumber(data.coder_parameter.cpu.value) * 500}m" "limits.cpu" = "${tonumber(data.coder_parameter.cpu.value) * 1000}m" "requests.memory" = "${tonumber(data.coder_parameter.memory.value) / 2}Mi" "limits.memory" = "${data.coder_parameter.memory.value}Mi" } } } # ============================================================================= # Coder Agent # ============================================================================= resource "coder_agent" "main" { arch = data.coder_provisioner.me.arch os = "linux" dir = "/home/coder" startup_script_behavior = "non-blocking" # Configure auto-stop TTL in Coder template settings: # Settings > Auto-stop > Default TTL: 1 hour # This ensures automated workspaces don't run indefinitely. metadata { display_name = "Task Type" key = "task_type" script = "echo $TASK_TYPE" interval = 0 } metadata { display_name = "Issue Number" key = "issue_number" script = "echo $ISSUE_NUMBER" interval = 0 } display_apps { vscode = true vscode_insiders = false ssh_helper = true port_forwarding_helper = true web_terminal = true } metadata { display_name = "Task Status" key = "task_status" script = <<-EOS if [ -f ~/task-output.log ]; then if grep -q "Task completed" ~/task-output.log 2>/dev/null; then echo "✅ Complete" else echo "⏳ Running ($(wc -l < ~/task-output.log) lines)" fi elif [ -n "$TASK_TYPE" ]; then echo "⏳ Starting..." else echo "Interactive" fi EOS interval = 10 } env = { CLAUDE_CODE_OAUTH_TOKEN = data.coder_parameter.claude_oauth_token.value GITHUB_TOKEN = data.coder_external_auth.github.access_token GITEA_TOKEN = data.coder_parameter.gitea_token.value GITEA_ORG = data.coder_parameter.gitea_org.value GITEA_REPO = data.coder_parameter.gitea_repo.value TASK_TYPE = data.coder_parameter.task_type.value ISSUE_NUMBER = data.coder_parameter.issue_number.value REPO_CLONE_URL = data.coder_parameter.repo_clone_url.value DEPLOY_ENV = data.coder_parameter.deploy_env.value CALLBACK_URL = data.coder_parameter.callback_url.value } startup_script = <<-EOT #!/bin/bash # Always notify the orchestrator when done, even on failure notify_complete() { if [ -n "$CALLBACK_URL" ]; then curl -s -X POST -H "Content-Type: application/json" \ -d "{\"status\":\"$1\"}" \ "$CALLBACK_URL" || echo "Callback failed (non-fatal)" fi } trap 'notify_complete "failed"' ERR EXIT set -e # --- Configure git --- git config --global user.name "${data.coder_workspace_owner.me.full_name}" git config --global user.email "${data.coder_workspace_owner.me.email}" # Configure git to use GITEA_TOKEN for HTTPS push/pull if [ -n "$GITEA_TOKEN" ]; then git config --global credential.helper 'store' echo "https://oauth2:$GITEA_TOKEN@gitea.samson.media" > ~/.git-credentials chmod 600 ~/.git-credentials fi # --- Clone repository --- if [ -n "$REPO_CLONE_URL" ]; then git clone "$REPO_CLONE_URL" ~/project cd ~/project # Switch to develop branch if it exists git fetch origin develop 2>/dev/null && git checkout develop 2>/dev/null || true # Lightweight stages only need the code, not a full build LIGHT_STAGES="analyse architect release maintenance" if echo "$LIGHT_STAGES" | grep -qw "$TASK_TYPE"; then echo "Lightweight stage ($TASK_TYPE) — skipping build" else # Install deps if package.json exists if [ -f package.json ]; then npm ci --legacy-peer-deps || npm ci fi # Install frontend deps if present if [ -f frontend/package.json ]; then cd frontend && npm ci && cd .. npm run build:frontend 2>/dev/null || true fi # Install Playwright if needed if grep -q "playwright" package.json 2>/dev/null; then npx playwright install chromium fi # Apply local migrations if script exists if npm run --silent db:migrate:local 2>/dev/null; then echo "Local migrations applied" fi fi fi # --- Automated task execution --- if [ -n "$TASK_TYPE" ] && [ -d ~/project ]; then cd ~/project ARGS="$ISSUE_NUMBER" if [ "$TASK_TYPE" = "devops" ] && [ -n "$DEPLOY_ENV" ]; then ARGS="$DEPLOY_ENV" fi # Read the slash command .md file, strip YAML frontmatter, substitute $ARGUMENTS CMD_FILE=".claude/commands/$TASK_TYPE.md" if [ ! -f "$CMD_FILE" ]; then echo "ERROR: Command file not found: $CMD_FILE" exit 1 fi PROMPT=$(sed '1{/^---$/!q}; 1,/^---$/d' "$CMD_FILE" | sed "s/\\\$ARGUMENTS/$ARGS/g") # Run Claude Code in non-interactive mode with tool access claude -p --dangerously-skip-permissions --verbose "$PROMPT" fi # Success — override the trap trap - ERR EXIT notify_complete "complete" EOT } # ============================================================================= # Main Workspace Deployment # ============================================================================= resource "kubernetes_deployment_v1" "workspace" { metadata { name = "workspace" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } wait_for_rollout = false spec { replicas = data.coder_workspace.me.start_count strategy { type = "Recreate" } selector { match_labels = { app = "workspace" } } template { metadata { labels = merge(local.labels, { app = "workspace" }) } spec { service_account_name = kubernetes_service_account_v1.workspace.metadata[0].name security_context { run_as_user = 1000 run_as_group = 1000 fs_group = 1000 } image_pull_secrets { name = kubernetes_secret_v1.registry.metadata[0].name } container { name = "coder-agent" image = "registry.samson.media/coder-workspace:latest" command = ["sh", "-c", coder_agent.main.init_script] env { name = "CODER_AGENT_TOKEN" value = coder_agent.main.token } env { name = "CLAUDE_CODE_OAUTH_TOKEN" value = data.coder_parameter.claude_oauth_token.value } env { name = "GITHUB_TOKEN" value = data.coder_external_auth.github.access_token } env { name = "GITEA_TOKEN" value = data.coder_parameter.gitea_token.value } env { name = "GITEA_ORG" value = data.coder_parameter.gitea_org.value } env { name = "GITEA_REPO" value = data.coder_parameter.gitea_repo.value } env { name = "TASK_TYPE" value = data.coder_parameter.task_type.value } env { name = "ISSUE_NUMBER" value = data.coder_parameter.issue_number.value } env { name = "REPO_CLONE_URL" value = data.coder_parameter.repo_clone_url.value } env { name = "DEPLOY_ENV" value = data.coder_parameter.deploy_env.value } env { name = "CALLBACK_URL" value = data.coder_parameter.callback_url.value } resources { requests = { cpu = "${tonumber(data.coder_parameter.cpu.value) * 500}m" memory = "${tonumber(data.coder_parameter.memory.value) / 2}Mi" } limits = { cpu = "${tonumber(data.coder_parameter.cpu.value) * 1000}m" memory = "${data.coder_parameter.memory.value}Mi" } } volume_mount { name = "home" mount_path = "/home/coder" } security_context { run_as_user = 1000 run_as_group = 1000 } } volume { name = "home" empty_dir {} } } } } }