# ============================================================================= # 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" } } data "coder_parameter" "disk_size" { name = "disk_size" display_name = "Disk Size (GB)" description = "Persistent home directory size" type = "number" default = "10" mutable = false option { name = "5 GB" value = "5" } option { name = "10 GB" value = "10" } option { name = "20 GB" value = "20" } } # ─── 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" "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" "requests.storage" = "${data.coder_parameter.disk_size.value}Gi" "persistentvolumeclaims" = "1" } } } # ============================================================================= # Storage # ============================================================================= resource "kubernetes_persistent_volume_claim_v1" "home" { metadata { name = "home" namespace = kubernetes_namespace_v1.workspace.metadata[0].name labels = local.labels } wait_until_bound = false spec { access_modes = ["ReadWriteOnce"] storage_class_name = "longhorn-backup" resources { requests = { storage = "${data.coder_parameter.disk_size.value}Gi" } } } } # ============================================================================= # 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 = { ANTHROPIC_API_KEY = data.coder_parameter.anthropic_api_key.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 set -e # --- Install Node.js 22 --- if ! command -v node &> /dev/null; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt-get install -y nodejs fi # --- Install global tools --- if ! command -v wrangler &> /dev/null; then sudo npm install -g wrangler @anthropic-ai/claude-code fi # --- Install Playwright system dependencies --- if ! dpkg -s libgbm1 &> /dev/null; then sudo apt-get update sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \ libcups2t64 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \ libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64 libatspi2.0-0 \ || sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \ libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \ libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0 fi # --- 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" ] && [ ! -d ~/project ]; 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 # Install backend deps npm ci --legacy-peer-deps # Install frontend deps cd frontend && npm ci && cd .. # Build frontend (required — vitest fails without frontend/dist) npm run build:frontend # Install Playwright Chromium npx playwright install chromium # Apply local migrations npm run db:migrate:local fi # --- Start log viewer on port 13338 --- if [ -n "$TASK_TYPE" ]; then touch ~/task-output.log cat > ~/log-server.sh << 'LOGEOF' #!/bin/bash while true; do { echo "HTTP/1.1 200 OK" echo "Content-Type: text/html; charset=utf-8" echo "Connection: close" echo "" echo "