diff --git a/servers/gcp/gitea-runner.tf b/servers/gcp/gitea-runner.tf index d3a9b30..6509e86 100644 --- a/servers/gcp/gitea-runner.tf +++ b/servers/gcp/gitea-runner.tf @@ -254,6 +254,46 @@ resource "google_service_account_iam_member" "ci_pusher_wif_neuron_web" { member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.gitea.name}/attribute.repository/neuron-technologies/neuron-web" } +# ── Gitea Actions runner on GKE ─────────────────────────────────────────────── +# A k8s Deployment in the `ci` namespace on GKE runs act_runner with a DinD +# sidecar. The pod uses this GCP SA via Workload Identity to pull the runner +# registration token from Secret Manager at startup. +# +# See servers/gcp/k8s/gitea-runner/ for the k8s manifests. + +resource "google_service_account" "gitea_runner_gke" { + account_id = "gitea-runner-gke" + display_name = "Gitea Actions runner (GKE identity)" + description = "Workload Identity SA for the GKE-hosted Gitea Actions runner. Read-only access to the registration token." + project = var.project_id +} + +# Allow the GKE SA to read the runner registration token from Secret Manager. +resource "google_secret_manager_secret_iam_member" "runner_gke_token_access" { + project = var.project_id + secret_id = "gitea-runner-token" + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${google_service_account.gitea_runner_gke.email}" +} + +# Workload Identity binding — the k8s SA `gitea-runner` in the `ci` namespace +# can impersonate this GCP SA without a JSON key file. +resource "google_service_account_iam_member" "gitea_runner_gke_workload_identity" { + service_account_id = google_service_account.gitea_runner_gke.name + role = "roles/iam.workloadIdentityUser" + member = "serviceAccount:${var.project_id}.svc.id.goog[ci/gitea-runner]" + + depends_on = [google_container_cluster.neuron_platform] +} + +# Allow the runner to pull images from Artifact Registry (needed for build jobs +# that pull from neuron-* repos, and for the runner's own ci-base image). +resource "google_project_iam_member" "gitea_runner_gke_ar_reader" { + project = var.project_id + role = "roles/artifactregistry.reader" + member = "serviceAccount:${google_service_account.gitea_runner_gke.email}" +} + # ── Outputs ─────────────────────────────────────────────────────────────────── output "gitea_runner_vm_name" { diff --git a/servers/gcp/k8s/argocd-apps/gitea-runner-gke.yaml b/servers/gcp/k8s/argocd-apps/gitea-runner-gke.yaml new file mode 100644 index 0000000..ab67f62 --- /dev/null +++ b/servers/gcp/k8s/argocd-apps/gitea-runner-gke.yaml @@ -0,0 +1,26 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: gitea-runner-gke + namespace: argocd +spec: + project: default + source: + repoURL: https://git.neuralplatform.ai/will/infrastructure.git + targetRevision: main + path: servers/gcp/k8s/gitea-runner + destination: + server: https://34.63.89.52 + namespace: ci + ignoreDifferences: + - group: apps + kind: Deployment + jsonPointers: + - /status + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/servers/gcp/k8s/gitea-runner/deployment.yaml b/servers/gcp/k8s/gitea-runner/deployment.yaml new file mode 100644 index 0000000..7cc6659 --- /dev/null +++ b/servers/gcp/k8s/gitea-runner/deployment.yaml @@ -0,0 +1,121 @@ +--- +# Gitea Actions runner on GKE. +# +# Architecture: +# - init container registers the runner with Gitea (--no-interactive, idempotent) +# - main container runs act_runner daemon +# - docker:dind sidecar provides Docker for build jobs (privileged) +# - emptyDir /data persists runner state within a pod lifetime +# +# The runner uses Docker-in-Docker (DinD) rather than a host socket because +# GKE Autopilot does not expose the node's Docker socket. +# DinD requires privileged mode — the ci namespace has +# pod-security.kubernetes.io/enforce: privileged (see namespace.yaml). +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea-runner + namespace: ci + labels: + app: gitea-runner +spec: + replicas: 1 + selector: + matchLabels: + app: gitea-runner + template: + metadata: + labels: + app: gitea-runner + spec: + serviceAccountName: gitea-runner + securityContext: + runAsNonRoot: false + initContainers: + - name: register + image: us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest + command: ["/bin/sh", "-c"] + args: + - | + # Idempotent registration — skip if already registered (.runner file exists). + if [ -f /data/.runner ]; then + echo "Runner already registered, skipping." + else + act_runner register \ + --instance "${GITEA_INSTANCE_URL}" \ + --token "${GITEA_RUNNER_REGISTRATION_TOKEN}" \ + --name "gke-runner-$(hostname)" \ + --labels "ubuntu-latest:docker://us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest,ubuntu-24.04:docker://us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest,linux,x64" \ + --no-interactive + fi + cat > /data/config.yaml << 'CONFIGEOF' + runner: + capacity: 2 + timeout: 3h + container: + network: host + docker_host: "tcp://localhost:2375" + force_pull: true + valid_volumes: [] + default_image: "us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest" + env: + GITEA_SSH_PRIVATE_KEY: "" + BASH_ENV: "/usr/local/bin/git-ssh-init.sh" + CONFIGEOF + env: + - name: GITEA_INSTANCE_URL + value: "https://git.neuralplatform.ai" + - name: DOCKER_HOST + value: "tcp://localhost:2375" + envFrom: + - secretRef: + name: gitea-runner-token + volumeMounts: + - mountPath: /data + name: data + workingDir: /data + containers: + - name: runner + image: us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest + command: ["act_runner", "daemon", "--config", "/data/config.yaml"] + env: + - name: GITEA_INSTANCE_URL + value: "https://git.neuralplatform.ai" + - name: DOCKER_HOST + value: "tcp://localhost:2375" + envFrom: + - secretRef: + name: gitea-runner-token + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "4" + memory: "4Gi" + volumeMounts: + - mountPath: /data + name: data + workingDir: /data + - name: dind + image: docker:dind + securityContext: + privileged: true + env: + - name: DOCKER_TLS_CERTDIR + value: "" + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "4" + memory: "4Gi" + volumeMounts: + - mountPath: /var/lib/docker + name: docker-storage + volumes: + - name: data + emptyDir: {} + - name: docker-storage + emptyDir: {} diff --git a/servers/gcp/k8s/gitea-runner/external-secrets.yaml b/servers/gcp/k8s/gitea-runner/external-secrets.yaml new file mode 100644 index 0000000..6b0126b --- /dev/null +++ b/servers/gcp/k8s/gitea-runner/external-secrets.yaml @@ -0,0 +1,39 @@ +--- +# SecretStore for the CI namespace — uses GCP Secret Manager via Workload Identity. +# The gitea-runner-gke GCP SA has secretmanager.secretAccessor on gitea-runner-token +# (see servers/gcp/gitea-runner.tf). +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: gcp-secretmanager + namespace: ci +spec: + provider: + gcpsm: + projectID: neuron-785695 + auth: + workloadIdentity: + clusterLocation: us-central1 + clusterName: neuron-platform + serviceAccountRef: + name: gitea-runner + namespace: ci +--- +# Pull the Gitea runner registration token from Secret Manager into a k8s Secret. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: gitea-runner-token + namespace: ci +spec: + refreshInterval: 1h + secretStoreRef: + name: gcp-secretmanager + kind: SecretStore + target: + name: gitea-runner-token + creationPolicy: Owner + data: + - secretKey: GITEA_RUNNER_REGISTRATION_TOKEN + remoteRef: + key: gitea-runner-token diff --git a/servers/gcp/k8s/gitea-runner/namespace.yaml b/servers/gcp/k8s/gitea-runner/namespace.yaml new file mode 100644 index 0000000..1a4fa09 --- /dev/null +++ b/servers/gcp/k8s/gitea-runner/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ci + labels: + app.kubernetes.io/name: ci + # DinD (Docker-in-Docker) requires privileged pods. + pod-security.kubernetes.io/enforce: privileged diff --git a/servers/gcp/k8s/gitea-runner/serviceaccount.yaml b/servers/gcp/k8s/gitea-runner/serviceaccount.yaml new file mode 100644 index 0000000..b7e8dde --- /dev/null +++ b/servers/gcp/k8s/gitea-runner/serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: gitea-runner + namespace: ci + annotations: + # Workload Identity — allows ESO (and optionally the runner pod) to + # authenticate to GCP Secret Manager as the gitea-runner-gke GCP SA + # without a JSON key file. + # The GCP SA binding is in servers/gcp/gitea-runner.tf (gitea_runner_gke_workload_identity). + iam.gke.io/gcp-service-account: gitea-runner-gke@neuron-785695.iam.gserviceaccount.com