# fxhnt cockpit — Deployment with an in-pod Tailscale **userspace** sidecar (the proper exposure for this
# cluster). The pod CIDR (100.64.0.0/15) overlaps Tailscale's CGNAT range (100.64.0.0/10), so a kernel-mode
# (TS_USERSPACE=false) tailscale proxy hijacks cross-node pod routes and cannot reach the Service. Running
# tailscale in USERSPACE mode inside the cockpit pod avoids that entirely: it never touches kernel routes,
# and `tailscale serve` proxies the tailnet to 127.0.0.1:8080 (same pod, no cross-node hop). Reachable over
# the tailnet at https://fxhnt-dashboard.<tailnet>.ts.net (Tailscale-provisioned TLS via MagicDNS).
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fxhnt-dashboard
  namespace: foxhunt
  labels: { app.kubernetes.io/name: fxhnt-dashboard, app.kubernetes.io/part-of: foxhunt }
spec:
  replicas: 1
  selector: { matchLabels: { app.kubernetes.io/name: fxhnt-dashboard } }
  template:
    metadata:
      labels: { app.kubernetes.io/name: fxhnt-dashboard, app.kubernetes.io/part-of: foxhunt }   # part-of -> postgres ingress allow
    spec:
      serviceAccountName: tailscale-dashboard        # RBAC to read/write the TS state secret
      nodeSelector:
        k8s.scaleway.com/pool-name: large
      imagePullSecrets:
        - name: scw-registry
      initContainers:
        # git-sync (best-effort): clone latest master src -> /code so the cockpit runs it via PYTHONPATH=/code/src
        # (code-only deploys = fast `rollout restart`, no image rebuild). On failure/timeout /code/src is absent
        # and the cockpit falls back to the baked site-packages — no outage. Sparse-checkout src/ only. See dagster.yaml.
        - name: git-sync
          image: alpine/git:latest
          command: [/bin/sh, -c]
          args:
            - |
              mkdir -p /root/.ssh && cp /etc/git-ssh/ssh-privatekey /root/.ssh/id_ed25519 && chmod 600 /root/.ssh/id_ed25519
              printf 'Host *\n  StrictHostKeyChecking no\n  UserKnownHostsFile /dev/null\n' > /root/.ssh/config
              rm -rf /code/src
              if timeout 120 git clone --depth 1 --branch master --filter=blob:none --sparse \
                   "ssh://git@gitea-ssh.gitea.svc.cluster.local:22/gitadmin/fxhnt.git" /tmp/repo \
                 && (cd /tmp/repo && git sparse-checkout set src) && [ -d /tmp/repo/src ]; then
                cp -a /tmp/repo/src /code/src && echo "git-sync OK: $(cd /tmp/repo && git rev-parse --short HEAD)"
              else
                echo "git-sync FAILED/timed out — falling back to baked image (site-packages)"; rm -rf /code/src
              fi
              exit 0
          resources: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } }
          volumeMounts:
            - { name: code, mountPath: /code }
            - { name: git-ssh-key, mountPath: /etc/git-ssh, readOnly: true }
      containers:
        - name: cockpit
          image: rg.fr-par.scw.cloud/bizworx/fxhnt-cockpit:latest
          command: ["fxhnt", "serve", "--host", "0.0.0.0", "--port", "8080"]   # 0.0.0.0 so the kubelet probe (pod IP) + sidecar (localhost) both reach it
          env:
            - name: FXHNT_OPERATIONAL_DSN
              value: "postgresql+psycopg://foxhunt@postgres.foxhunt.svc.cluster.local:5432/fxhnt"
            - name: PGPASSWORD
              valueFrom: { secretKeyRef: { name: db-credentials, key: password } }
            - { name: PYTHONPATH, value: /code/src }   # git-sync'd code wins over baked image; empty -> baked fallback
            - { name: FXHNT_FEATURE_STORE, value: "postgres" }   # warehouse reads -> TimescaleDB (networked). Unset/=duckdb to roll back.
          volumeMounts:
            - { name: code, mountPath: /code, readOnly: true }
          readinessProbe: { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 5, periodSeconds: 10 }
          livenessProbe:  { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 10, periodSeconds: 20 }
          resources: { requests: { cpu: 250m, memory: 128Mi }, limits: { cpu: "2", memory: 512Mi } }   # cpu 2: the /paper/sim engine is CPU-bound (~1.2s on a full core); 500m throttled it ~7x
        - name: tailscale
          image: ghcr.io/tailscale/tailscale:latest
          env:
            - name: TS_AUTHKEY
              valueFrom: { secretKeyRef: { name: tailscale-auth, key: TS_AUTHKEY } }
            - name: TS_KUBE_SECRET
              value: fxhnt-dashboard-ts-state
            - name: TS_USERSPACE
              value: "true"                          # netstack — no kernel route hijack (CGNAT-safe)
            - name: TS_HOSTNAME
              value: "fxhnt-dashboard"               # -> fxhnt-dashboard.<tailnet>.ts.net
            - name: TS_SERVE_CONFIG
              value: /etc/ts/serve.json
          volumeMounts:
            - { name: ts-serve, mountPath: /etc/ts, readOnly: true }
          resources: { requests: { cpu: 25m, memory: 64Mi }, limits: { cpu: 200m, memory: 128Mi } }
      volumes:
        - name: ts-serve
          configMap: { name: fxhnt-dashboard-ts-serve }
        - { name: code, emptyDir: {} }                                          # git-sync target
        - { name: git-ssh-key, secret: { secretName: argo-git-ssh-key, defaultMode: 256 } }
---
# tailscale serve: terminate HTTPS on the node's MagicDNS cert, proxy everything to the local cockpit.
apiVersion: v1
kind: ConfigMap
metadata:
  name: fxhnt-dashboard-ts-serve
  namespace: foxhunt
data:
  serve.json: |
    {
      "TCP": {
        "443": { "HTTPS": true },
        "80": { "TCPForward": "127.0.0.1:8080" }
      },
      "Web": {
        "${TS_CERT_DOMAIN}:443": {
          "Handlers": { "/": { "Proxy": "http://127.0.0.1:8080" } }
        }
      }
    }
---
apiVersion: v1
kind: Service
metadata:
  name: fxhnt-dashboard
  namespace: foxhunt
spec:
  selector: { app.kubernetes.io/name: fxhnt-dashboard }
  ports: [{ port: 80, targetPort: 8080, name: http }]
