diff --git a/servers/gcp/cloud-sql.tf b/servers/gcp/cloud-sql.tf index f4c42eb..9947074 100644 --- a/servers/gcp/cloud-sql.tf +++ b/servers/gcp/cloud-sql.tf @@ -148,3 +148,100 @@ resource "google_secret_manager_secret" "license_admin_token" { ignore_changes = [id] } } + +# ── Gitea — database + user on neuron-prod-pg15 ─────────────────────────────── +# Gitea on GKE uses the existing Cloud SQL instance with a Cloud SQL Auth Proxy +# sidecar. Connection via unix socket — no public IP exposure. + +resource "google_sql_database" "gitea" { + name = "gitea" + instance = google_sql_database_instance.main.name + project = var.project_id +} + +resource "random_password" "gitea_db" { + length = 32 + special = false # Cloud SQL unix socket path DSN; keep alphanumeric for simplicity +} + +resource "google_sql_user" "gitea" { + name = "gitea" + instance = google_sql_database_instance.main.name + project = var.project_id + password = random_password.gitea_db.result +} + +# ── Gitea service account (for Workload Identity → Cloud SQL) ───────────────── + +resource "google_service_account" "gitea" { + account_id = "gitea" + display_name = "Gitea GKE SA" + description = "Service account for the Gitea pod on GKE. Used by the Cloud SQL Auth Proxy sidecar via Workload Identity." + project = var.project_id +} + +resource "google_project_iam_member" "gitea_sql_client" { + project = var.project_id + role = "roles/cloudsql.client" + member = "serviceAccount:${google_service_account.gitea.email}" +} + +# ── Secret Manager — Gitea database URL ─────────────────────────────────────── +# Full DSN using the Cloud SQL Auth Proxy unix socket path. +# The proxy sidecar mounts the socket at /cloudsql/. + +resource "google_secret_manager_secret" "gitea_database_url" { + secret_id = "gitea-database-url" + project = var.project_id + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "gitea_database_url" { + secret = google_secret_manager_secret.gitea_database_url.id + secret_data = "host=/cloudsql/${google_sql_database_instance.main.connection_name} user=gitea password=${random_password.gitea_db.result} dbname=gitea sslmode=disable" +} + +# gitea-db-password — the raw password only, for use in Gitea's GITEA__database__PASSWD. +# The Cloud SQL Auth Proxy provides the unix socket; Gitea uses standard postgres +# password auth through the proxy socket. +resource "google_secret_manager_secret" "gitea_db_password" { + secret_id = "gitea-db-password" + project = var.project_id + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "gitea_db_password" { + secret = google_secret_manager_secret.gitea_db_password.id + secret_data = random_password.gitea_db.result +} + +# Allow the Gitea GCP SA to access its secrets +resource "google_secret_manager_secret_iam_member" "gitea_database_url_accessor" { + project = var.project_id + secret_id = google_secret_manager_secret.gitea_database_url.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.gitea.email}" +} + +resource "google_secret_manager_secret_iam_member" "gitea_db_password_accessor" { + project = var.project_id + secret_id = google_secret_manager_secret.gitea_db_password.secret_id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.gitea.email}" +} + +output "gitea_service_account_email" { + description = "Gitea GKE SA email — used in the Workload Identity annotation" + value = google_service_account.gitea.email +} + +output "gitea_database_secret_id" { + description = "Secret Manager secret ID for the Gitea database URL" + value = google_secret_manager_secret.gitea_database_url.secret_id +} diff --git a/servers/gcp/gke.tf b/servers/gcp/gke.tf new file mode 100644 index 0000000..822b12b --- /dev/null +++ b/servers/gcp/gke.tf @@ -0,0 +1,94 @@ +# ── GKE Autopilot Cluster — neuron-platform ─────────────────────────────────── +# +# Regional Autopilot cluster in us-central1. Autopilot manages the node pool +# automatically — no node pools to configure, no VM sizes to choose. +# Pods spread across a/b/c zones automatically by GKE. +# +# Workload Identity is required so Vault pods can access KMS (auto-unseal) +# and Gitea pods can use Cloud SQL Auth Proxy without key files. +# +# After `terraform apply`, register the cluster with Legion Argo CD: +# 1. Get the endpoint: terraform output gke_cluster_endpoint +# 2. Get credentials: gcloud container clusters get-credentials neuron-platform \ +# --region us-central1 --project neuron-785695 +# 3. Register with Argo CD: +# argocd cluster add --name gke-neuron-platform +# 4. Update servers/gcp/k8s/argocd-apps/*.yaml with the actual cluster endpoint. + +resource "google_container_cluster" "neuron_platform" { + provider = google-beta + + name = "neuron-platform" + location = "us-central1" + project = var.project_id + + # Autopilot — GKE manages node pools, scaling, and node security + enable_autopilot = true + + release_channel { + channel = "REGULAR" + } + + # Workload Identity — required for Vault KMS and Gitea Cloud SQL proxy + workload_identity_config { + workload_pool = "${var.project_id}.svc.id.goog" + } + + # Deletion protection — prevent accidental cluster destruction + deletion_protection = true + + # Cluster networking — uses default VPC (same as Vault GCE nodes) + ip_allocation_policy {} + + # Disable basic auth and client cert (Workload Identity handles auth) + master_auth { + client_certificate_config { + issue_client_certificate = false + } + } +} + +# ── Workload Identity bindings ──────────────────────────────────────────────── +# +# Each GCP SA gets a binding that allows its corresponding k8s ServiceAccount +# (in the GKE cluster) to impersonate it via Workload Identity. +# The k8s SA must also have the annotation: +# iam.gke.io/gcp-service-account: + +# Vault pods (namespace: vault, k8s SA: vault) → vault-unseal GCP SA +# This lets the Vault StatefulSet access KMS for auto-unseal without key files. +resource "google_service_account_iam_member" "vault_workload_identity" { + service_account_id = google_service_account.vault_unseal.name + role = "roles/iam.workloadIdentityUser" + member = "serviceAccount:${var.project_id}.svc.id.goog[vault/vault]" + + depends_on = [google_container_cluster.neuron_platform] +} + +# Gitea pod (namespace: gitea, k8s SA: gitea) → gitea GCP SA +# This lets the Cloud SQL Auth Proxy sidecar connect to neuron-prod-pg15. +resource "google_service_account_iam_member" "gitea_workload_identity" { + service_account_id = google_service_account.gitea.name + role = "roles/iam.workloadIdentityUser" + member = "serviceAccount:${var.project_id}.svc.id.goog[gitea/gitea]" + + depends_on = [google_container_cluster.neuron_platform] +} + +# ── Outputs ─────────────────────────────────────────────────────────────────── + +output "gke_cluster_endpoint" { + description = "GKE cluster API endpoint — use as destination.server in Argo CD Applications" + value = "https://${google_container_cluster.neuron_platform.endpoint}" + sensitive = true +} + +output "gke_cluster_name" { + description = "GKE cluster name" + value = google_container_cluster.neuron_platform.name +} + +output "gke_workload_pool" { + description = "Workload Identity pool — used in Workload Identity bindings" + value = "${var.project_id}.svc.id.goog" +} diff --git a/servers/gcp/k8s/argocd-apps/gitea-gke.yaml b/servers/gcp/k8s/argocd-apps/gitea-gke.yaml new file mode 100644 index 0000000..952e6b3 --- /dev/null +++ b/servers/gcp/k8s/argocd-apps/gitea-gke.yaml @@ -0,0 +1,38 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea-gke + namespace: argocd + annotations: + # Apply to the Legion Argo CD instance after registering the GKE cluster. + # See vault-gke.yaml for the cluster registration steps. + # + # Migration checklist (Gitea from Legion to GKE): + # 1. terraform apply (creates GKE cluster, Cloud SQL gitea DB, secrets) + # 2. Register GKE cluster with Legion Argo CD (see vault-gke.yaml) + # 3. Install ESO on GKE: helm install external-secrets external-secrets/external-secrets + # --namespace external-secrets --create-namespace + # 4. Apply this Application to Legion Argo CD + # 5. Verify gitea pod is running on GKE with DB connectivity + # 6. Take a tar of /data from the Legion Gitea pod and restore to GKE PVC + # 7. Update Cloudflare Tunnel on Legion: remove git.neuralplatform.ai route + # 8. Add GKE ingress / GCP LB rule for git.neuralplatform.ai + # 9. Decommission Gitea on Legion (remove gitea*.yaml from servers/legion/apps/) +spec: + project: default + source: + repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git + targetRevision: main + path: servers/gcp/k8s/gitea + destination: + # Replace GKE_CLUSTER_ENDPOINT after `terraform apply`: + # terraform -chdir=servers/gcp output -raw gke_cluster_endpoint + server: https://GKE_CLUSTER_ENDPOINT + namespace: gitea + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/servers/gcp/k8s/argocd-apps/vault-gke.yaml b/servers/gcp/k8s/argocd-apps/vault-gke.yaml new file mode 100644 index 0000000..c4caec6 --- /dev/null +++ b/servers/gcp/k8s/argocd-apps/vault-gke.yaml @@ -0,0 +1,33 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault-gke + namespace: argocd + annotations: + # Syncs plain k8s manifests for Vault to the GKE cluster. + # The Vault Helm release itself is in vault-helm-gke.yaml. + # + # Apply to Legion Argo CD after registering the GKE cluster: + # 1. gcloud container clusters get-credentials neuron-platform \ + # --region us-central1 --project neuron-785695 + # 2. argocd cluster add --name gke-neuron-platform + # 3. Update GKE_CLUSTER_ENDPOINT in all argocd-apps/*.yaml: + # terraform -chdir=servers/gcp output -raw gke_cluster_endpoint +spec: + project: default + source: + repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git + targetRevision: main + path: servers/gcp/k8s/vault + destination: + # Replace GKE_CLUSTER_ENDPOINT after `terraform apply`: + # terraform -chdir=servers/gcp output -raw gke_cluster_endpoint + server: https://GKE_CLUSTER_ENDPOINT + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/servers/gcp/k8s/argocd-apps/vault-helm-gke.yaml b/servers/gcp/k8s/argocd-apps/vault-helm-gke.yaml new file mode 100644 index 0000000..2a9fe69 --- /dev/null +++ b/servers/gcp/k8s/argocd-apps/vault-helm-gke.yaml @@ -0,0 +1,153 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: vault-helm-gke + namespace: argocd + annotations: + # Deploys the Vault Helm chart to the GKE cluster via Legion Argo CD. + # The destination.server must be updated after `terraform apply`: + # terraform -chdir=servers/gcp output -raw gke_cluster_endpoint +spec: + project: default + source: + repoURL: https://helm.releases.hashicorp.com + chart: vault + targetRevision: "0.29.1" + helm: + values: | + global: + enabled: true + + injector: + enabled: false + + ui: + enabled: true + + server: + image: + repository: hashicorp/vault + tag: "1.19.2" + + # Workload Identity — Vault pod k8s SA impersonates vault-unseal GCP SA + # for KMS auto-unseal. Binding is in servers/gcp/gke.tf. + serviceAccount: + create: true + name: vault + annotations: + iam.gke.io/gcp-service-account: vault-unseal@neuron-785695.iam.gserviceaccount.com + + # GKE Autopilot: no privileged containers. Vault doesn't need privilege. + # Request IPC_LOCK so Vault can lock memory (prevents secrets swap). + securityContext: + capabilities: + add: + - IPC_LOCK + + # HA mode — 3 replicas with Raft storage + ha: + enabled: true + replicas: 3 + raft: + enabled: true + setNodeId: true + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + + storage "raft" { + path = "/vault/data" + + retry_join { + leader_api_addr = "http://vault-0.vault-internal:8200" + } + retry_join { + leader_api_addr = "http://vault-1.vault-internal:8200" + } + retry_join { + leader_api_addr = "http://vault-2.vault-internal:8200" + } + } + + seal "gcpckms" { + project = "neuron-785695" + region = "global" + key_ring = "vault" + crypto_key = "vault-unseal" + } + + telemetry { + prometheus_retention_time = "30s" + disable_hostname = false + } + + # Spread pods across GKE zones + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: vault + component: server + + # 10Gi SSD per pod — premium-rwo = pd-ssd on GKE Autopilot + dataStorage: + enabled: true + size: 10Gi + storageClass: premium-rwo + accessMode: ReadWriteOnce + + readinessProbe: + enabled: true + path: "/v1/sys/health?standbyok=true&sealedok=true&uninitcode=200" + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 3 + + livenessProbe: + enabled: true + path: "/v1/sys/health?standbyok=true&sealedok=true&uninitcode=200" + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 3 + + # GKE Autopilot requires resource requests on all containers + resources: + requests: + memory: 256Mi + cpu: 250m + limits: + memory: 512Mi + cpu: 500m + + service: + enabled: true + type: ClusterIP + port: 8200 + targetPort: 8200 + + # Ingress disabled — Vault is exposed via GCP HTTPS LB. + # After migration, update the existing LB backend (vault-nodes.tf) + # to target a GKE NEG instead of the GCE instance groups. + # See: https://cloud.google.com/kubernetes-engine/docs/how-to/standalone-neg + ingress: + enabled: false + + destination: + # Replace GKE_CLUSTER_ENDPOINT after `terraform apply`: + # terraform -chdir=servers/gcp output -raw gke_cluster_endpoint + server: https://GKE_CLUSTER_ENDPOINT + namespace: vault + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/servers/gcp/k8s/gitea/configmap.yaml b/servers/gcp/k8s/gitea/configmap.yaml new file mode 100644 index 0000000..03c6a92 --- /dev/null +++ b/servers/gcp/k8s/gitea/configmap.yaml @@ -0,0 +1,104 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: gitea-custom-css + namespace: gitea +data: + header.tmpl: | + + custom.css: | + /* ── Typography & base ── */ + :root { + --color-primary: #6366f1; + --color-primary-dark: #4f46e5; + --color-secondary: #8b5cf6; + --color-bg: #0f0f17; + --color-surface: #16161f; + --color-surface-2: #1e1e2e; + --color-border: #2a2a3d; + --color-text: #e2e2f0; + --color-text-muted: #8888aa; + --color-green: #22d3a5; + --radius: 10px; + } + + body { + background: var(--color-bg) !important; + color: var(--color-text) !important; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; + } + + /* ── Top navbar ── */ + .navbar, #navbar { + background: var(--color-surface) !important; + border-bottom: 1px solid var(--color-border) !important; + box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important; + } + + .navbar .brand svg, .navbar .brand img { filter: brightness(1.2); } + + /* ── Sidebar & panels ── */ + .repository, .ui.container, .ui.segment, + .ui.card, .ui.cards > .card { + background: var(--color-surface) !important; + border: 1px solid var(--color-border) !important; + border-radius: var(--radius) !important; + } + + /* ── Buttons ── */ + .ui.primary.button, .ui.green.button { + background: var(--color-primary) !important; + border: none !important; + border-radius: 8px !important; + } + .ui.primary.button:hover { background: var(--color-primary-dark) !important; } + + /* ── Inputs ── */ + input, textarea, select, + .ui.input > input, .ui.dropdown { + background: var(--color-surface-2) !important; + border: 1px solid var(--color-border) !important; + color: var(--color-text) !important; + border-radius: 8px !important; + } + + /* ── Sign-in page ── */ + .user.signin .ui.segment, + .user.signup .ui.segment { + background: var(--color-surface) !important; + border: 1px solid var(--color-border) !important; + border-radius: 16px !important; + box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important; + padding: 2.5rem !important; + } + + /* ── Repo file tree ── */ + .repository.file.list .file-list { + background: var(--color-surface) !important; + border-radius: var(--radius) !important; + border: 1px solid var(--color-border) !important; + } + + .repository.file.list .file-list tr:hover td { + background: var(--color-surface-2) !important; + } + + /* ── Labels & badges ── */ + .ui.label { border-radius: 6px !important; } + + /* ── Dashboard activity feed ── */ + .feeds .news { border-bottom: 1px solid var(--color-border) !important; } + + /* ── Code blocks ── */ + pre, code { + background: var(--color-surface-2) !important; + border: 1px solid var(--color-border) !important; + border-radius: 6px !important; + } + + /* ── Muted footer ── */ + #footer { + background: var(--color-surface) !important; + border-top: 1px solid var(--color-border) !important; + color: var(--color-text-muted) !important; + } diff --git a/servers/gcp/k8s/gitea/deployment.yaml b/servers/gcp/k8s/gitea/deployment.yaml new file mode 100644 index 0000000..edb5e37 --- /dev/null +++ b/servers/gcp/k8s/gitea/deployment.yaml @@ -0,0 +1,155 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea + namespace: gitea + labels: + app: gitea +spec: + replicas: 1 + # Recreate — only one pod can hold the RWO PVC at a time. + # Scale to multiple replicas only after adding shared storage (e.g. Filestore). + strategy: + type: Recreate + selector: + matchLabels: + app: gitea + template: + metadata: + labels: + app: gitea + spec: + serviceAccountName: gitea + + containers: + - name: gitea + image: gitea/gitea:1.25.5 + ports: + - name: http + containerPort: 3000 + - name: ssh + containerPort: 22 + env: + # Database — connect through the Cloud SQL Auth Proxy unix socket + - name: GITEA__database__DB_TYPE + value: postgres + # Unix socket path used by the Cloud SQL Auth Proxy sidecar + - name: GITEA__database__HOST + value: /cloudsql/neuron-785695:us-central1:neuron-prod-pg15 + - name: GITEA__database__NAME + value: gitea + - name: GITEA__database__USER + value: gitea + - name: GITEA__database__PASSWD + valueFrom: + secretKeyRef: + name: gitea-db + key: password + # Server + - name: GITEA__server__DOMAIN + value: git.neuralplatform.ai + - name: GITEA__server__ROOT_URL + value: https://git.neuralplatform.ai + - name: GITEA__server__SSH_DOMAIN + value: git.neuralplatform.ai + - name: GITEA__server__SSH_PORT + value: "22" + - name: GITEA__server__START_SSH_SERVER + value: "false" + # Service + - name: GITEA__service__DISABLE_REGISTRATION + value: "true" + - name: GITEA__service__REQUIRE_SIGNIN_VIEW + value: "false" + # Security + - name: GITEA__security__INSTALL_LOCK + value: "true" + # Packages + - name: GITEA__packages__ENABLED + value: "true" + # Webhooks — allow calls back into the cluster and GKE VPC + - name: GITEA__webhook__ALLOWED_HOST_LIST + value: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 + # Actions + - name: GITEA__actions__DEFAULT_ACTIONS_URL + value: https://code.forgejo.org + volumeMounts: + - name: data + mountPath: /data + - name: cloudsql-socket + mountPath: /cloudsql + - name: custom-css + mountPath: /data/gitea/custom/public/assets/css/custom.css + subPath: custom.css + - name: custom-css + mountPath: /data/gitea/custom/templates/custom/header.tmpl + subPath: header.tmpl + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 15 + failureThreshold: 3 + + # Cloud SQL Auth Proxy sidecar — provides unix socket at /cloudsql/ + # Authenticates to Cloud SQL using Workload Identity (no key file). + - name: cloud-sql-proxy + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2 + args: + - "--structured-logs" + - "--unix-socket=/cloudsql" + - "neuron-785695:us-central1:neuron-prod-pg15" + securityContext: + runAsNonRoot: true + volumeMounts: + - name: cloudsql-socket + mountPath: /cloudsql + resources: + requests: + memory: 32Mi + cpu: 10m + limits: + memory: 128Mi + cpu: 100m + + volumes: + - name: data + persistentVolumeClaim: + claimName: gitea-data + - name: cloudsql-socket + emptyDir: {} + - name: custom-css + configMap: + name: gitea-custom-css +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea + namespace: gitea +spec: + selector: + app: gitea + ports: + - name: http + port: 3000 + targetPort: 3000 + - name: ssh + port: 22 + targetPort: 22 + type: ClusterIP diff --git a/servers/gcp/k8s/gitea/external-secrets.yaml b/servers/gcp/k8s/gitea/external-secrets.yaml new file mode 100644 index 0000000..07b13a0 --- /dev/null +++ b/servers/gcp/k8s/gitea/external-secrets.yaml @@ -0,0 +1,68 @@ +--- +# SecretStore for GKE — uses GCP Secret Manager directly via Workload Identity. +# On GKE we use the GCP provider instead of a Vault-backed store, since +# Vault itself may be in the process of being migrated. +# The Gitea GCP SA has secretmanager.secretAccessor on its own secret (see cloud-sql.tf). +# +# Pre-requisite: install ESO on GKE before applying this: +# helm install external-secrets external-secrets/external-secrets \ +# --namespace external-secrets --create-namespace +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: gcp-secretmanager + namespace: gitea +spec: + provider: + gcpsm: + projectID: neuron-785695 + # Workload Identity is used automatically — no explicit auth needed + # when the pod's ServiceAccount has the iam.gke.io/gcp-service-account annotation. +--- +# gitea-db — Gitea database password, pulled from GCP Secret Manager. +# The Secret Manager secret "gitea-database-url" stores the full DSN, but we +# extract just the password field for use in GITEA__database__PASSWD. +# +# The full DSN format from Terraform: +# host=/cloudsql/ user=gitea password= dbname=gitea sslmode=disable +# +# ESO extracts the raw secret value. Since GCP Secret Manager stores the full +# DSN as a single string, we store the password separately as "gitea-db-password" +# so Gitea can receive it as a discrete env var. +# +# Bootstrap: after `terraform apply`, run: +# PASSWORD=$(gcloud secrets versions access latest --secret=gitea-database-url \ +# | grep -oP '(?<=password=)\S+') +# echo -n "$PASSWORD" | gcloud secrets create gitea-db-password \ +# --data-file=- --project=neuron-785695 +# +# Or simpler — let Terraform write it directly. The gitea-db-password secret +# is managed by the gitea-database-url secret version output. Use the full DSN +# secret and parse in-pod, or store password separately. +# +# For simplicity: pull the full DSN and use it as GITEA__database__PASSWD +# is wrong (it's a DSN, not a password). Instead, use the Cloud SQL proxy +# unix socket and no password — configure Gitea to use peer auth. +# +# ACTUAL APPROACH: ExternalSecret pulls the full DSN string into a k8s Secret +# key "dsn". A separate gitea-db secret provides just the password field. +# Terraform outputs both; add a gitea-db-password Secret Manager secret in cloud-sql.tf. +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: gitea-db + namespace: gitea +spec: + refreshInterval: 1h + secretStoreRef: + name: gcp-secretmanager + kind: SecretStore + target: + name: gitea-db + creationPolicy: Owner + data: + - secretKey: password + remoteRef: + # This secret is populated by Terraform (gitea-db-password in cloud-sql.tf) + # It contains just the raw database password (no DSN prefix). + key: gitea-db-password diff --git a/servers/gcp/k8s/gitea/namespace.yaml b/servers/gcp/k8s/gitea/namespace.yaml new file mode 100644 index 0000000..c202c9a --- /dev/null +++ b/servers/gcp/k8s/gitea/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gitea + labels: + app.kubernetes.io/name: gitea diff --git a/servers/gcp/k8s/gitea/pvc.yaml b/servers/gcp/k8s/gitea/pvc.yaml new file mode 100644 index 0000000..819f812 --- /dev/null +++ b/servers/gcp/k8s/gitea/pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-data + namespace: gitea +spec: + # standard-rwo = pd-balanced on GKE Autopilot (ReadWriteOnce) + # Use premium-rwo (pd-ssd) if repo performance becomes a bottleneck. + storageClassName: standard-rwo + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi diff --git a/servers/gcp/k8s/gitea/serviceaccount.yaml b/servers/gcp/k8s/gitea/serviceaccount.yaml new file mode 100644 index 0000000..a0956f1 --- /dev/null +++ b/servers/gcp/k8s/gitea/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gitea + namespace: gitea + annotations: + # Workload Identity — allows the Cloud SQL Auth Proxy sidecar to authenticate + # to Cloud SQL as the gitea GCP SA without a JSON key file. + # The GCP SA binding is in servers/gcp/gke.tf (gitea_workload_identity). + iam.gke.io/gcp-service-account: gitea@neuron-785695.iam.gserviceaccount.com diff --git a/servers/gcp/k8s/vault/namespace.yaml b/servers/gcp/k8s/vault/namespace.yaml new file mode 100644 index 0000000..158c301 --- /dev/null +++ b/servers/gcp/k8s/vault/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vault + labels: + app.kubernetes.io/name: vault diff --git a/servers/gcp/vault-nodes.tf b/servers/gcp/vault-nodes.tf index 601de00..8e7d8c6 100644 --- a/servers/gcp/vault-nodes.tf +++ b/servers/gcp/vault-nodes.tf @@ -1,3 +1,12 @@ +# PENDING DECOMMISSION — migrating to GKE Autopilot (gke.tf). Remove after Vault data migrated. +# Migration checklist: +# 1. Bring up Vault StatefulSet on GKE (servers/gcp/k8s/vault/) +# 2. Take a raft snapshot from the active GCE node: vault operator raft snapshot save vault.snap +# 3. Restore snapshot into the new GKE Vault: vault operator raft snapshot restore vault.snap +# 4. Validate all secrets are accessible from the GKE cluster +# 5. Update Cloudflare DNS / LB to point vault.neuralplatform.ai at GKE ingress +# 6. Remove this file and the vault-kms.tf GCE-specific IAM (keep the KMS key + SA) +# # ── Vault HA Cluster — GCE-based Raft ──────────────────────────────────────── # # Three GCE e2-small VMs across us-central1-{a,b,c} running HashiCorp Vault diff --git a/servers/legion/apps/gke-apps.yaml b/servers/legion/apps/gke-apps.yaml new file mode 100644 index 0000000..9ce6819 --- /dev/null +++ b/servers/legion/apps/gke-apps.yaml @@ -0,0 +1,22 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gke-apps + namespace: argocd +spec: + project: default + source: + repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git + targetRevision: main + path: servers/gcp/k8s/argocd-apps + destination: + # This Application lives on Legion Argo CD — it creates child Applications + # there that target the GKE cluster. + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/k8s/gitea-runner/Dockerfile b/servers/legion/k8s/gitea-runner/Dockerfile index e2c3780..59f8d1d 100644 --- a/servers/legion/k8s/gitea-runner/Dockerfile +++ b/servers/legion/k8s/gitea-runner/Dockerfile @@ -95,5 +95,18 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ # We deliberately don't set ENTRYPOINT / CMD here — act_runner spawns # build containers with its own entrypoint to keep them alive between # steps, and overriding it breaks job execution. + +# In-cluster git redirect — GKE runners resolve the internal Gitea service +# directly, bypassing Cloudflare Access. This insteadOf rule rewrites any +# clone/fetch of the public URL to the in-cluster service URL so CI jobs work +# without CF Access headers inside the GKE cluster. +# +# On Legion runners this is overridden at runtime by git-cf-access-init.sh +# (which sets the reverse redirect + CF Access headers). The system-level rule +# here is a safe baseline; the init script wins because it runs after. +RUN git config --system \ + url."http://gitea.gitea.svc.cluster.local:3000/".insteadOf \ + "https://git.neuralplatform.ai/" + COPY git-cf-access-init.sh /usr/local/bin/git-cf-access-init.sh RUN chmod +x /usr/local/bin/git-cf-access-init.sh