Lightweight Hono webhook router that receives Gitea events and creates Coder workspaces for each SDLC stage (analyse, architect, develop, review, test, rework, release, maintenance). Includes k8s manifests for deployment and the Coder workspace template moved from babble. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
692 lines
18 KiB
Terraform
692 lines
18 KiB
Terraform
# =============================================================================
|
|
# 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" "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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
|
|
# --- 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
|
|
|
|
# Run Claude Code in non-interactive mode
|
|
claude --print --dangerously-skip-permissions "/project:$TASK_TYPE $ARGS" 2>&1 | tee ~/task-output.log
|
|
|
|
echo "Task completed. Output saved to ~/task-output.log"
|
|
fi
|
|
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
|
|
}
|
|
|
|
init_container {
|
|
name = "fix-permissions"
|
|
image = "busybox:latest"
|
|
|
|
command = ["sh", "-c", <<-EOC
|
|
if [ ! -d /home/coder/project ]; then
|
|
mkdir -p /home/coder/project
|
|
fi
|
|
chown -R 1000:1000 /home/coder
|
|
EOC
|
|
]
|
|
|
|
volume_mount {
|
|
name = "home"
|
|
mount_path = "/home/coder"
|
|
}
|
|
|
|
resources {
|
|
requests = {
|
|
cpu = "5m"
|
|
memory = "8Mi"
|
|
}
|
|
limits = {
|
|
cpu = "50m"
|
|
memory = "32Mi"
|
|
}
|
|
}
|
|
|
|
security_context {
|
|
run_as_user = 0
|
|
}
|
|
}
|
|
|
|
container {
|
|
name = "coder-agent"
|
|
image = "codercom/enterprise-base:ubuntu-arm64"
|
|
|
|
command = ["sh", "-c", coder_agent.main.init_script]
|
|
|
|
env {
|
|
name = "CODER_AGENT_TOKEN"
|
|
value = coder_agent.main.token
|
|
}
|
|
|
|
env {
|
|
name = "ANTHROPIC_API_KEY"
|
|
value = data.coder_parameter.anthropic_api_key.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
|
|
}
|
|
|
|
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"
|
|
persistent_volume_claim {
|
|
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata[0].name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|