# Argo WorkflowTemplate — build + deploy the fxhnt cockpit, fully in-cluster.
#
# Reuses cluster primitives: the `argo-workflow` ServiceAccount (foxhunt, see fxhnt-ci-rbac.yaml), the
# `argo-git-ssh-key` deploy key (clone the fxhnt repo over in-cluster Gitea SSH, gitea-ssh.gitea.svc:22), and
# the `scw-registry` dockerconfig (push to the external Scaleway Container Registry rg.fr-par.scw.cloud/bizworx).
#
# Submit:  argo submit -n foxhunt --from=wftmpl/fxhnt-cockpit -p commit-sha=<sha> -p mode=<auto|build|deploy> --watch
# Normally auto-triggered: a push to gitadmin/fxhnt master -> the build-fxhnt Sensor (argo-events) submits this.
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: fxhnt-cockpit
  namespace: foxhunt
  labels: { app.kubernetes.io/name: fxhnt-cockpit, app.kubernetes.io/part-of: foxhunt }
spec:
  serviceAccountName: argo-workflow
  entrypoint: pipeline
  archiveLogs: false           # archive build/deploy logs to MinIO (argo-logs bucket). Reachable now that the
                              # pods carry app.kubernetes.io/part-of: argo-workflows (MinIO ingress netpol allow).
  activeDeadlineSeconds: 1800
  ttlStrategy: { secondsAfterCompletion: 3600 }
  podGC: { strategy: OnPodCompletion }
  arguments:
    parameters:
      - { name: commit-sha, value: HEAD }
      - { name: mode, value: auto }   # "auto" = decide-mode git-diffs commit-sha~1..commit-sha to pick build/deploy; "build" = force kaniko rebuild + deploy; "deploy" = force skip build, just apply + rollout restart (git-sync re-pulls code). A human can still force build/deploy via `argo submit -p mode=...`.
  templates:
    - name: pipeline
      dag:
        tasks:
          # decide-mode: when mode=auto, git-diffs the pushed commit against its parent to pick build vs deploy
          # (dep/Dockerfile changes -> build; anything else, or a missing parent, -> build/deploy per script logic
          # below). When mode is forced (build|deploy), decide-mode just echoes it back unchanged.
          - { name: decide-mode, template: decide-mode }
          # Skip the ~5min kaniko build for code-only pushes (mode=deploy) — git-sync re-pulls src/ on the
          # rollout restart, so only dep/Dockerfile changes need a new image (mode=build).
          - { name: build, template: kaniko-build, depends: decide-mode, when: "{{tasks.decide-mode.outputs.parameters.mode}} == build" }
          - { name: deploy, template: kubectl-deploy, depends: "decide-mode && (build.Succeeded || build.Skipped)" }

    # --- decide-mode: resolve "auto" to build|deploy via git diff of the pushed commit vs its parent ---
    - name: decide-mode
      metadata:
        labels: { app.kubernetes.io/part-of: argo-workflows }
      nodeSelector: { k8s.scaleway.com/pool-name: large }
      tolerations: [{ effect: NoSchedule, key: node.cilium.io/agent-not-ready, operator: Exists }]
      outputs:
        parameters:
          - name: mode
            valueFrom: { path: /workspace/mode }
      container:
        image: alpine/git:latest
        command: [/bin/sh, -c]
        # SECURITY: commit-sha/mode arrive via ENV (not inline {{...}} substitution) so an untrusted webhook
        # payload cannot inject shell. commit-sha is validated hex before use; mode is checked against an allowlist.
        env:
          - { name: COMMIT_SHA, value: "{{workflow.parameters.commit-sha}}" }
          - { name: MODE, value: "{{workflow.parameters.mode}}" }
          # Absolute key path via GIT_SSH_COMMAND — Argo's emissary executor does not set HOME=/root, so the
          # old `cp key -> ~/.ssh` idiom left git unable to find the key (clone failed with exit 128). This is
          # HOME-independent and verified to clone under HOME=/nonexistent.
          - { name: GIT_SSH_COMMAND, value: "ssh -i /etc/git-ssh/ssh-privatekey -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" }
        args:
          - |
            set -e
            case "$COMMIT_SHA" in *[!0-9a-fA-F]*|"") echo "refusing non-hex commit-sha" >&2; exit 1 ;; esac
            case "$MODE" in auto|build|deploy) : ;; *) echo "refusing unknown mode: $MODE" >&2; exit 1 ;; esac
            git clone --filter=blob:none \
              "ssh://git@gitea-ssh.gitea.svc.cluster.local:22/gitadmin/fxhnt.git" /workspace/src
            cd /workspace/src && git checkout "$COMMIT_SHA"

            if [ "$MODE" != "auto" ]; then
              printf '%s' "$MODE" > /workspace/mode
            else
              RESOLVED="$(git rev-parse "$COMMIT_SHA")"
              if CHANGED=$(git diff --name-only "${RESOLVED}~1" "${RESOLVED}" 2>/dev/null); then
                # diff succeeded (parent exists): build only if a dep/Dockerfile file changed, else deploy.
                if echo "$CHANGED" | grep -qE '^pyproject\.toml$|^infra/docker/fxhnt\.Dockerfile$'; then
                  printf 'build' > /workspace/mode
                else
                  printf 'deploy' > /workspace/mode
                fi
              else
                # diff failed (no parent -- first commit / shallow history): default to build (safe, never wrong).
                printf 'build' > /workspace/mode
              fi
            fi
            cat /workspace/mode
        volumeMounts:
          - { name: git-ssh-key, mountPath: /etc/git-ssh, readOnly: true }
          - { name: workspace, mountPath: /workspace }
      volumes:
        - { name: workspace, emptyDir: {} }
        - { name: git-ssh-key, secret: { secretName: argo-git-ssh-key, defaultMode: 256 } }

    # --- build: Kaniko builds infra/docker/fxhnt.Dockerfile -> internal registry ---
    - name: kaniko-build
      metadata:
        labels: { app.kubernetes.io/part-of: argo-workflows }   # MinIO ingress allows part-of in {foxhunt,foxhunt-ci,argo-workflows} → log archival works. NOT 'foxhunt' (that adds argo-base-egress which blocks registry:5000/git:2222).
      nodeSelector: { k8s.scaleway.com/pool-name: large }
      tolerations: [{ effect: NoSchedule, key: node.cilium.io/agent-not-ready, operator: Exists }]
      initContainers:
        - name: git-clone
          image: alpine/git:latest
          command: [/bin/sh, -c]
          # SECURITY: commit-sha via ENV + hex-validated (see decide-mode note) — no inline shell substitution.
          env:
            - { name: COMMIT_SHA, value: "{{workflow.parameters.commit-sha}}" }
            # Absolute key path via GIT_SSH_COMMAND — HOME-independent (Argo's emissary doesn't set HOME=/root).
            - { name: GIT_SSH_COMMAND, value: "ssh -i /etc/git-ssh/ssh-privatekey -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" }
          args:
            - |
              set -ex
              case "$COMMIT_SHA" in *[!0-9a-fA-F]*|"") echo "refusing non-hex commit-sha" >&2; exit 1 ;; esac
              git clone --no-checkout --filter=blob:none \
                "ssh://git@gitea-ssh.gitea.svc.cluster.local:22/gitadmin/fxhnt.git" /workspace/src
              cd /workspace/src && git checkout "$COMMIT_SHA"
              echo "checked out $(git rev-parse --short HEAD)"
          volumeMounts:
            - { name: git-ssh-key, mountPath: /etc/git-ssh, readOnly: true }
            - { name: workspace, mountPath: /workspace }
      container:
        image: gcr.io/kaniko-project/executor:v1.23.2
        # Kaniko's own entrypoint + a YAML args LIST (the msp build-image pattern) — NOT `sh -c` with one big
        # string. A shell wrapper made busybox try to parse the `{{=sprig.trunc(...)}}` expansion and choke on
        # the `(` (syntax error). As a plain args list, Argo evaluates the expression and hands it straight to
        # Kaniko, no shell in between.
        args:
          - --context=/workspace/src
          - --dockerfile=/workspace/src/infra/docker/fxhnt.Dockerfile
          - --destination=rg.fr-par.scw.cloud/bizworx/fxhnt-cockpit:latest
          - --destination=rg.fr-par.scw.cloud/bizworx/fxhnt-cockpit:{{workflow.parameters.commit-sha}}
          - --cache=true
          - --cache-repo=rg.fr-par.scw.cloud/bizworx/cache
          - --snapshot-mode=redo
        env:
          - { name: DOCKER_CONFIG, value: /kaniko/.docker }
        resources: { requests: { cpu: "1", memory: 3Gi }, limits: { cpu: "3", memory: 10Gi } }
        volumeMounts:
          - { name: registry-auth, mountPath: /kaniko/.docker, readOnly: true }
          - { name: workspace, mountPath: /workspace }
      volumes:
        - { name: workspace, emptyDir: {} }
        - { name: git-ssh-key, secret: { secretName: argo-git-ssh-key, defaultMode: 256 } }
        - name: registry-auth
          secret:
            secretName: scw-registry
            items: [{ key: .dockerconfigjson, path: config.json }]

    # --- deploy: apply manifests from the repo + roll the dashboard to the fresh image ---
    - name: kubectl-deploy
      metadata:
        labels: { app.kubernetes.io/part-of: argo-workflows }   # MinIO ingress allow → log archival works (see kaniko-build note)
      nodeSelector: { k8s.scaleway.com/pool-name: large }
      serviceAccountName: argo-workflow
      initContainers:
        - name: git-clone
          image: alpine/git:latest
          command: [/bin/sh, -c]
          # SECURITY: commit-sha via ENV + hex-validated — no inline shell substitution.
          env:
            - { name: COMMIT_SHA, value: "{{workflow.parameters.commit-sha}}" }
            # Absolute key path via GIT_SSH_COMMAND — HOME-independent (Argo's emissary doesn't set HOME=/root).
            - { name: GIT_SSH_COMMAND, value: "ssh -i /etc/git-ssh/ssh-privatekey -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" }
          args:
            - |
              set -ex
              case "$COMMIT_SHA" in *[!0-9a-fA-F]*|"") echo "refusing non-hex commit-sha" >&2; exit 1 ;; esac
              git clone --no-checkout --filter=blob:none \
                "ssh://git@gitea-ssh.gitea.svc.cluster.local:22/gitadmin/fxhnt.git" /workspace/src
              cd /workspace/src && git checkout "$COMMIT_SHA"
          volumeMounts:
            - { name: git-ssh-key, mountPath: /etc/git-ssh, readOnly: true }
            - { name: workspace, mountPath: /workspace }
      container:
        image: alpine/k8s:1.31.0   # Alpine-based (has /bin/sh + kubectl). NOT registry.k8s.io/kubectl — that's distroless (no shell) and the deploy runs a shell script. bitnami/kubectl was removed from Docker Hub (2025).
        command: [/bin/sh, -c]
        args:
          - |
            set -e
            cd /workspace/src
            kubectl apply -f infra/k8s/services/fxhnt-dashboard.yaml
            kubectl apply -f infra/k8s/orchestration/dagster.yaml
            kubectl apply -f infra/k8s/network-policies/fxhnt-cockpit.yaml
            kubectl -n foxhunt annotate deploy/fxhnt-dashboard deploy/dagster \
              fxhnt.io/deployed-sha="$(cd /workspace/src && git rev-parse --short HEAD)" --overwrite
            # Dashboard: scale 0 -> 1, NOT `rollout restart`. Its tailscale sidecar serves dashboard.fxhnt.ai;
            # a rolling restart briefly overlaps two tailscale sessions on the same identity -> session race ->
            # the external proxy link goes stale and the cockpit reads "down" post-deploy. Scaling to 0 cleanly
            # terminates the session before a fresh, non-overlapping one comes up; the git-sync initContainer
            # re-pulls src the same way, so code still updates. See reference_fxhnt_dashboard_proxy.
            #
            # SAFETY (CI review I1): once we scale to 0, a trap guarantees the deployment is scaled BACK to 1 on
            # ANY exit path — so a failure between scale-0 and scale-1 (transient apiserver error, throttle) can
            # never strand the production dashboard at 0 replicas. The trap is best-effort (its own failure is
            # swallowed) and cleared on success.
            trap 'kubectl -n foxhunt scale deploy/fxhnt-dashboard --replicas=1 || true' EXIT
            kubectl -n foxhunt scale deploy/fxhnt-dashboard --replicas=0
            kubectl -n foxhunt wait --for=delete pod -l app.kubernetes.io/name=fxhnt-dashboard --timeout=90s || true
            kubectl -n foxhunt scale deploy/fxhnt-dashboard --replicas=1
            trap - EXIT   # scaled back up cleanly; no longer need the safety net
            kubectl -n foxhunt rollout restart deploy/dagster        # dagster has no external-proxy race
            kubectl -n foxhunt rollout status deploy/fxhnt-dashboard --timeout=180s
            kubectl -n foxhunt rollout status deploy/dagster --timeout=300s
        volumeMounts:
          - { name: workspace, mountPath: /workspace }
      volumes:
        - { name: workspace, emptyDir: {} }
        - { name: git-ssh-key, secret: { secretName: argo-git-ssh-key, defaultMode: 256 } }
