diff --git a/docs/superpowers/plans/2026-06-21-gitea-replace-gitlab.md b/docs/superpowers/plans/2026-06-21-gitea-replace-gitlab.md index 02f5ecf29..a683a31c7 100644 --- a/docs/superpowers/plans/2026-06-21-gitea-replace-gitlab.md +++ b/docs/superpowers/plans/2026-06-21-gitea-replace-gitlab.md @@ -130,8 +130,8 @@ gitea: ROOT_URL: https://git.fxhnt.ai/ DOMAIN: git.fxhnt.ai SSH_DOMAIN: git.fxhnt.ai - SSH_PORT: "22" # advertise clean git@git.fxhnt.ai in NEW clone URLs; legacy :2222 still - # works via the proxy (Task 5). Port 22 is free — nothing host-SSHes the git node. + SSH_PORT: "22" # clean git@git.fxhnt.ai clone URLs. Port 22 is free (nothing host-SSHes + # the git node); :2222 retired, local remotes updated in Task 5 Step 2b. DISABLE_SSH: "false" database: DB_TYPE: postgres @@ -297,28 +297,26 @@ to: Delete the entire `listen 5050 ssl; server_name git.fxhnt.ai;` registry block (Scaleway registry is external — not proxied). -- [ ] **Step 2: Repoint the socat SSH proxy → Gitea SSH, on BOTH `:22` and `:2222`** +- [ ] **Step 2: Repoint the socat SSH proxy to Gitea SSH on `:22` (drop `:2222`)** -Nothing ever SSHes into the git node itself, so port `22` is free for git-SSH. Expose Gitea SSH on both -the standard `:22` (clean `git@git.fxhnt.ai` URLs) and the legacy `:2222` (existing remotes keep working — -no churn). In the `ssh-proxy` container args, change the existing `:2222` target to Gitea: +Nothing host-SSHes the git node, so use the standard port `22` and retire `:2222` entirely (clean +`git@git.fxhnt.ai` URLs). In the `ssh-proxy` container args, change the listen port to `22` and the target +to Gitea: ``` - - "TCP-LISTEN:2222,fork,reuseaddr,sndbuf=1048576,rcvbuf=1048576" - - "TCP:gitea-ssh.foxhunt.svc.cluster.local:22,sndbuf=1048576,rcvbuf=1048576" -``` -Then add a SECOND socat container in the same pod for `:22` (duplicate the `ssh-proxy` container, rename -`ssh-proxy-22`, listen on `22`, `containerPort: 22`): -```yaml - - name: ssh-proxy-22 - image: alpine/socat:latest - args: - "TCP-LISTEN:22,fork,reuseaddr,sndbuf=1048576,rcvbuf=1048576" - "TCP:gitea-ssh.foxhunt.svc.cluster.local:22,sndbuf=1048576,rcvbuf=1048576" - ports: - - containerPort: 22 ``` -Ensure the Tailscale node/proxy advertises both ports `22` and `2222` (the tailnet sidecar's exposed -ports / `serve` config — add `22` alongside the existing `2222`). +Update the container `ports:` entry from `containerPort: 2222` → `containerPort: 22`. Ensure the Tailscale +node/proxy advertises port `22` (update the tailnet sidecar's exposed ports / `serve` config: `22`, +replacing `2222`). + +- [ ] **Step 2b: Update local git remotes from `:2222` → default (port 22)** +```bash +cd /home/jgrusewski/Work/foxhunt && git remote set-url origin ssh://git@git.fxhnt.ai/gitadmin/foxhunt.git +cd /home/jgrusewski/Work/fxhnt && git remote set-url origin ssh://git@git.fxhnt.ai/gitadmin/fxhnt.git +cd /home/jgrusewski/Work/foxhunt && git remote -v | grep fetch +``` +Expected: remotes now show `ssh://git@git.fxhnt.ai/...` (no `:2222`). - [ ] **Step 3: Apply + restart the proxy** ```bash @@ -332,11 +330,10 @@ Expected: rollout complete. - [ ] **Step 4: GATE — verify `git.fxhnt.ai` serves Gitea** ```bash curl -sk https://git.fxhnt.ai/api/healthz | head -c 200; echo -echo "--- SSH on :2222 (legacy remotes) ---"; git ls-remote ssh://git@git.fxhnt.ai:2222/gitadmin/fxhnt.git HEAD 2>&1 | head -1 -echo "--- SSH on :22 (clean URLs) ---"; git ls-remote ssh://git@git.fxhnt.ai/gitadmin/fxhnt.git HEAD 2>&1 | head -1 +echo "--- SSH on :22 (git@git.fxhnt.ai) ---"; git ls-remote ssh://git@git.fxhnt.ai/gitadmin/fxhnt.git HEAD 2>&1 | head -1 ``` -Expected: Gitea healthz JSON; BOTH ssh ls-remotes (`:2222` and `:22`) return the HEAD sha (proves Gitea -web + SSH on both ports on the canonical name). **STOP + rollback (revert this file, re-apply) if any fail.** +Expected: Gitea healthz JSON; the `:22` ssh ls-remote returns the HEAD sha (proves Gitea web + SSH on the +canonical name). **STOP + rollback (revert this file, re-apply) if either fails.** - [ ] **Step 5: Commit (foxhunt repo)** ```bash diff --git a/docs/superpowers/specs/2026-06-21-gitea-replace-gitlab-design.md b/docs/superpowers/specs/2026-06-21-gitea-replace-gitlab-design.md index c1a156b74..34ac8729d 100644 --- a/docs/superpowers/specs/2026-06-21-gitea-replace-gitlab-design.md +++ b/docs/superpowers/specs/2026-06-21-gitea-replace-gitlab-design.md @@ -24,9 +24,9 @@ breaking the live cockpit deploy. via `git push --mirror` from existing local clones. - **Canonical hostname stays `git.fxhnt.ai`** (no permanent `gitea.fxhnt.ai`, no remote-URL churn). Gitea comes up internal-only; validate over a port-forward; then cut `git.fxhnt.ai` (HTTPS) + SSH over - to Gitea in one switch. **SSH exposed on BOTH `:22` and `:2222`** — nothing host-SSHes the git node so - `:22` is free; Gitea advertises the clean `git@git.fxhnt.ai` (`:22`) in new clone URLs while existing - `:2222` remotes keep working (no churn). GitLab keeps running (unaddressed) as the 2C-removal fallback. + to Gitea in one switch. **SSH on `:22`** (nothing host-SSHes the git node, so the standard port is free); + clone URLs become the clean `git@git.fxhnt.ai` and `:2222` is retired (local remotes updated once). + GitLab keeps running (unaddressed) as the 2C-removal fallback. - **Container registry:** cockpit image → **Scaleway Container Registry** (`rg.fr-par.scw.cloud/bizworx/`, namespace exists). Retire the in-cluster GitLab registry. External/managed, no registry pod. - **Argo integration:** Gitea push webhook → Argo Events webhook eventsource → sensor → submit @@ -44,8 +44,8 @@ breaking the live cockpit deploy. 3. **Exposure:** - HTTPS: extend the nginx `*.fxhnt.ai` proxy (`infra/k8s/gitlab/tailscale-proxy.yaml`) — at cutover, repoint the `git.fxhnt.ai` server block `proxy_pass` → `gitea-http.foxhunt.svc:3000`. - - SSH: expose Gitea SSH on BOTH `:22` and `:2222` via the Tailscale proxy (two socat listeners) at - cutover; advertise `:22` in clone URLs, keep `:2222` for existing remotes. + - SSH: expose Gitea SSH on `:22` via the Tailscale proxy (socat) at cutover; retire `:2222`; update the + two local remotes to `ssh://git@git.fxhnt.ai/...`. - Registry `:5050` server block: removed (Scaleway registry is external, not proxied here). 4. **Registry migration:** - `fxhnt/infra/argo/cockpit-build-deploy.yaml`: kaniko `--destination` + `--cache-repo` → diff --git a/infra/k8s/argo/events/gitea-deploy-sensor.yaml b/infra/k8s/argo/events/gitea-deploy-sensor.yaml new file mode 100644 index 000000000..0719b97bb --- /dev/null +++ b/infra/k8s/argo/events/gitea-deploy-sensor.yaml @@ -0,0 +1,56 @@ +# On push to fxhnt main/master in Gitea, submit the fxhnt-cockpit build+deploy workflow (Phase 2B). +# Gitea push payload: { "ref": "refs/heads/main", "after": "", ... } +apiVersion: argoproj.io/v1alpha1 +kind: Sensor +metadata: + name: gitea-deploy + namespace: foxhunt + labels: + app.kubernetes.io/name: gitea-deploy + app.kubernetes.io/part-of: foxhunt +spec: + template: + # pod intentionally NOT part-of:foxhunt (avoids default-deny-all; matches working sensors) + serviceAccountName: argo-workflow + nodeSelector: + k8s.scaleway.com/pool-name: platform + dependencies: + - name: gitea-push-dep + eventSourceName: gitea-push + eventName: push + filters: + data: + - path: body.ref + type: string + value: + - "refs/heads/main" + - "refs/heads/master" + triggers: + - template: + name: deploy-cockpit + conditions: gitea-push-dep + argoWorkflow: + operation: submit + source: + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: fxhnt-cockpit- + namespace: foxhunt + spec: + serviceAccountName: argo-workflow + podGC: + strategy: OnPodCompletion + ttlStrategy: + secondsAfterCompletion: 3600 + workflowTemplateRef: + name: fxhnt-cockpit + arguments: + parameters: + - name: commit-sha + parameters: + - src: + dependencyName: gitea-push-dep + dataKey: body.after + dest: spec.arguments.parameters.0.value diff --git a/infra/k8s/argo/events/gitea-push-eventsource.yaml b/infra/k8s/argo/events/gitea-push-eventsource.yaml new file mode 100644 index 000000000..670d3ca1a --- /dev/null +++ b/infra/k8s/argo/events/gitea-push-eventsource.yaml @@ -0,0 +1,28 @@ +# Gitea push webhook → Argo Events. Replaces the old gitlab-push eventsource (Phase 2B). +# Gitea posts to gitea-push-eventsource-svc:12000/push on push to the fxhnt repo. +apiVersion: argoproj.io/v1alpha1 +kind: EventSource +metadata: + name: gitea-push + namespace: foxhunt + labels: + app.kubernetes.io/name: gitea-push + app.kubernetes.io/part-of: foxhunt +spec: + template: + # NOTE: pod is intentionally NOT part-of:foxhunt — that label triggers default-deny-all. The working + # workflow-trigger/gitlab-push eventsource pods have no part-of, so they stay default-allow. + metadata: + labels: + app.kubernetes.io/name: gitea-push + nodeSelector: + k8s.scaleway.com/pool-name: platform + service: + ports: + - port: 12000 + targetPort: 12000 + webhook: + push: + port: "12000" + endpoint: /push + method: POST diff --git a/infra/k8s/gitea/clusterip-services.yaml b/infra/k8s/gitea/clusterip-services.yaml new file mode 100644 index 000000000..6056b2eb4 --- /dev/null +++ b/infra/k8s/gitea/clusterip-services.yaml @@ -0,0 +1,40 @@ +# The gitea chart's gitea-http/gitea-ssh services are HEADLESS (clusterIP: None), so they resolve to the +# pod IP (100.64.x.x). That range OVERLAPS the Tailscale CGNAT range (100.64.0.0/10), so the tailscale +# sidecar in the tailscale-gitlab-proxy pod swallows traffic to the pod IP. These normal ClusterIP services +# give Gitea a service IP in the service CIDR (10.32.x.x, outside the tailscale range) — the proxy nginx + +# socat target these instead. (GitLab worked because its webservice svc was a normal ClusterIP.) +apiVersion: v1 +kind: Service +metadata: + name: gitea-web + namespace: foxhunt + labels: + app.kubernetes.io/name: gitea +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: gitea + ports: + - name: http + port: 3000 + targetPort: 3000 + protocol: TCP +--- +apiVersion: v1 +kind: Service +metadata: + name: gitea-sshd + namespace: foxhunt + labels: + app.kubernetes.io/name: gitea +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: gitea + ports: + - name: ssh + port: 22 + targetPort: 2222 # rootless gitea listens on 2222 internally (can't bind privileged 22) + protocol: TCP diff --git a/infra/k8s/gitea/networkpolicy.yaml b/infra/k8s/gitea/networkpolicy.yaml new file mode 100644 index 000000000..0cef92109 --- /dev/null +++ b/infra/k8s/gitea/networkpolicy.yaml @@ -0,0 +1,58 @@ +# Gitea NetworkPolicy (foxhunt ns has default-deny-all). Egress: postgres + DNS + in-cluster webhook +# target + general HTTPS. Ingress: from platform/argo pods + the tailscale proxy → :3000 (web) and :22 (ssh). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: gitea + namespace: foxhunt +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: gitea + policyTypes: + - Ingress + - Egress + ingress: + # web (3000) + ssh (22) from platform + argo-workflow pods (cockpit build clones over HTTP) + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/part-of + operator: In + values: ["foxhunt", "argo-workflows"] + ports: + - { port: 3000, protocol: TCP } + - { port: 2222, protocol: TCP } + # web + ssh from the tailscale proxy (nginx + socat) that fronts git.fxhnt.ai + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: tailscale-gitlab-proxy + ports: + - { port: 3000, protocol: TCP } + - { port: 2222, protocol: TCP } + egress: + # PostgreSQL + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: postgres + ports: + - { port: 5432, protocol: TCP } + # DNS + - ports: + - { port: 53, protocol: UDP } + - { port: 53, protocol: TCP } + # Argo Events webhook target (gitea push hook → gitea-push eventsource :12000). The eventsource pod is + # not part-of:foxhunt (stays default-allow), so allow egress to any in-namespace pod on 12000. + - to: + - podSelector: {} + ports: + - { port: 12000, protocol: TCP } + # general HTTPS (avatars, external clones) — external only, exclude cluster CIDRs + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: ["10.32.0.0/16", "172.16.0.0/16"] + ports: + - { port: 443, protocol: TCP } diff --git a/infra/k8s/gitea/values.yaml b/infra/k8s/gitea/values.yaml new file mode 100644 index 000000000..13f6775b5 --- /dev/null +++ b/infra/k8s/gitea/values.yaml @@ -0,0 +1,73 @@ +# Gitea — lightweight git host replacing GitLab (Phase 2B). External Postgres (existing in-cluster +# `postgres`), no bundled DB/redis/memcached, Actions off (Argo does CI). Internal-only until cutover. +replicaCount: 1 +image: + rootless: true + +# RWO PVC (sbs-default-retain) → only one pod can mount it. RollingUpdate deadlocks (new pod can't +# attach while old holds it). Recreate terminates old before starting new. +strategy: + type: Recreate + +# Disable all bundled subcharts — reuse the existing in-cluster postgres, no redis/memcached. +postgresql: + enabled: false +postgresql-ha: + enabled: false +redis-cluster: + enabled: false +redis: + enabled: false +# chart 12.x replaced redis with valkey — disable both, gitea uses embedded queue/cache/session +valkey-cluster: + enabled: false +valkey: + enabled: false + +persistence: + enabled: true + size: 5Gi + storageClass: sbs-default-retain + +resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 2Gi } # 2Gi headroom: indexing the 876MB foxhunt pack OOMed at 512Mi + +service: + http: { type: ClusterIP, port: 3000 } + ssh: { type: ClusterIP, port: 22 } + +gitea: + admin: + existingSecret: gitea-admin + config: + actions: + ENABLED: "false" # Argo does CI/CD; Gitea Actions off (subchart removed in chart 12.x) + server: + ROOT_URL: https://git.fxhnt.ai/ + DOMAIN: git.fxhnt.ai + SSH_DOMAIN: git.fxhnt.ai + SSH_PORT: "22" # clean git@git.fxhnt.ai clone URLs; :2222 retired (Task 5). + DISABLE_SSH: "false" + database: + DB_TYPE: postgres + HOST: postgres.foxhunt.svc.cluster.local:5432 + NAME: gitea + USER: gitea + service: + DISABLE_REGISTRATION: "true" + webhook: + # allow delivery to in-cluster targets (Argo Events eventsource on the 10.32.x service CIDR); + # default blocks private hosts. Argo auto-deploy webhook needs this. + ALLOWED_HOST_LIST: "private" + # No redis/memcached — use embedded adapters + cache: + ADAPTER: memory + session: + PROVIDER: db + queue: + TYPE: level + additionalConfigFromEnvs: + - name: GITEA__database__PASSWD + valueFrom: + secretKeyRef: { name: gitea-db, key: password } diff --git a/infra/k8s/gitlab/tailscale-proxy.yaml b/infra/k8s/gitlab/tailscale-proxy.yaml index 6d5cf239a..2a202425d 100644 --- a/infra/k8s/gitlab/tailscale-proxy.yaml +++ b/infra/k8s/gitlab/tailscale-proxy.yaml @@ -75,10 +75,10 @@ spec: - name: ssh-proxy image: alpine/socat:latest args: - - "TCP-LISTEN:2222,fork,reuseaddr,sndbuf=1048576,rcvbuf=1048576" - - "TCP:gitlab-gitlab-shell.foxhunt.svc.cluster.local:2222,sndbuf=1048576,rcvbuf=1048576" + - "TCP-LISTEN:22,fork,reuseaddr,sndbuf=1048576,rcvbuf=1048576" + - "TCP:gitea-sshd.foxhunt.svc.cluster.local:22,sndbuf=1048576,rcvbuf=1048576" ports: - - containerPort: 2222 + - containerPort: 22 resources: requests: cpu: 25m @@ -152,7 +152,7 @@ data: return 301 https://$host$request_uri; } - # GitLab — git.fxhnt.ai + # Gitea — git.fxhnt.ai (replaced GitLab, Phase 2B cutover) server { listen 443 ssl; server_name git.fxhnt.ai; @@ -164,7 +164,7 @@ data: client_max_body_size 0; location / { - proxy_pass http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181; + proxy_pass http://gitea-web.foxhunt.svc.cluster.local:3000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/infra/k8s/network-policies/infrastructure.yaml b/infra/k8s/network-policies/infrastructure.yaml index 3f8beeac4..59278dc25 100644 --- a/infra/k8s/network-policies/infrastructure.yaml +++ b/infra/k8s/network-policies/infrastructure.yaml @@ -1,4 +1,4 @@ -# Postgres: accepts connections from foxhunt app pods, GitLab, and Grafana +# Postgres: accepts connections from foxhunt app pods, GitLab, Grafana, and Gitea apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -23,6 +23,9 @@ spec: - podSelector: matchLabels: app.kubernetes.io/name: grafana + - podSelector: # Gitea (Phase 2B) reuses the in-cluster postgres + matchLabels: + app.kubernetes.io/name: gitea ports: - protocol: TCP port: 5432 diff --git a/infra/live/production/dns/terragrunt.hcl b/infra/live/production/dns/terragrunt.hcl index e9e9e3ff7..26fb9efbb 100644 --- a/infra/live/production/dns/terragrunt.hcl +++ b/infra/live/production/dns/terragrunt.hcl @@ -8,6 +8,7 @@ terraform { inputs = { dns_zone = "fxhnt.ai" - # Tailscale IP of foxhunt-gitlab proxy pod - git_ip = "100.90.76.85" + # Tailscale IP of the foxhunt-gitlab proxy pod (live node foxhunt-gitlab; the old 100.90.76.85 + # was a stale prior IP — its duplicate A records across *.fxhnt.ai were removed 2026-06-21). + git_ip = "100.95.225.27" }