name: Deploy to Talos # A merge to main ships the stack to the Talos cluster on the lab server # (docs/runbooks/kubernetes-talos.md §9). PR CI is the merge gate, so main is # green by construction — this workflow only deploys. on: push: branches: [main] workflow_dispatch: permissions: contents: read # Queue deploys, never cancel one: a helm upgrade killed half-way leaves the # release in `pending-upgrade` and the next run has to be unwedged by hand. concurrency: group: deploy-talos cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest env: # The Talos VM as seen from the Fedora host (libvirt guest IP), and the # address a browser uses to reach the cluster. `localhost` is deliberate: # the portals' PKCE needs a secure context, so they are reached over # `kubectl port-forward` — runbook §5. Override with repo variables. TALOS_VM_IP: ${{ vars.TALOS_VM_IP }} TALOS_HOST: ${{ vars.TALOS_HOST }} # Set it and the stack is published over TLS on . by the # in-cluster edge (ADR-0035, runbook §10). Empty = NodePorts, as before. PUBLIC_DOMAIN: ${{ vars.PUBLIC_DOMAIN }} PUBLIC_EMAIL: ${{ vars.PUBLIC_EMAIL }} steps: - uses: https://github.com/actions/checkout@v4 # Pinned static binaries, the same URLs the Talos runbook §0 gives a # developer and the same helm the `k8s` CI job uses — no action to vet. - name: Install kubectl, helm and crane run: | set -euo pipefail bin="$HOME/.local/bin"; mkdir -p "$bin" curl -sSLo "$bin/kubectl" https://dl.k8s.io/release/v1.37.0/bin/linux/amd64/kubectl curl -sSL https://get.helm.sh/helm-v3.16.4-linux-amd64.tar.gz | tar xz -O linux-amd64/helm > "$bin/helm" curl -sSL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xz -O crane > "$bin/crane" chmod +x "$bin"/{kubectl,helm,crane} echo "$bin" >> "$GITHUB_PATH" # The cluster's API and its registry are only reachable through the Fedora # host, so forward both to the runner. 30141 is the openbaar portal, for # the smoke at the end. - name: Tunnel the Talos API + registry through the Fedora host env: SSH_KEY: ${{ secrets.TALOS_SSH_KEY }} run: | set -euo pipefail : "${TALOS_VM_IP:=192.168.122.173}" umask 077 printf '%s\n' "$SSH_KEY" > ~/.ssh_talos ssh -i ~/.ssh_talos -o StrictHostKeyChecking=no -o IdentitiesOnly=yes \ -o ExitOnForwardFailure=yes -p 6667 -f -N \ -L 6443:$TALOS_VM_IP:6443 \ -L 30500:$TALOS_VM_IP:30500 \ -L 30141:$TALOS_VM_IP:30141 \ user@labs.respellion.tech # The kubeconfig's server must be https://127.0.0.1:6443 — Talos puts # 127.0.0.1 in the apiserver cert SANs, so TLS verification still holds # through the tunnel. - name: Write the kubeconfig env: KUBECONFIG_B64: ${{ secrets.TALOS_KUBECONFIG }} run: | set -euo pipefail umask 077 base64 -d <<< "$KUBECONFIG_B64" > "$RUNNER_TEMP/kubeconfig" echo "KUBECONFIG=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_ENV" kubectl --kubeconfig "$RUNNER_TEMP/kubeconfig" get nodes # Idempotent; also makes a first deploy onto a bare cluster work. The # registry's storage is an emptyDir, so a replaced pod loses the images — # which the push in the next step puts back anyway. - name: Ensure the in-cluster registry run: make k8s-registry # Push through the tunnel (localhost), pull from the node's own NodePort # (the address in the Talos registry-mirror patch) — same registry, two # names, so the two `make` calls get different K8S_REGISTRY values. - name: Build and push the images run: make k8s-images K8S_REGISTRY=localhost:30500 # k8s-reseed = seed configmaps + helm upgrade + re-run the bootstrap jobs. # The jobs are idempotent, and deleting them first is what keeps a changed # Job template from wedging the upgrade (`cannot patch … with kind Job`). - name: Deploy the chart run: | set -euo pipefail publish="${PUBLIC_DOMAIN:+--set public.domain=$PUBLIC_DOMAIN --set public.email=${PUBLIC_EMAIL:-}}" make k8s-reseed \ TALOS_HOST=${TALOS_HOST:-localhost} \ K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \ K8S_SET="$publish" # `dev` is a mutable tag and helm sees an unchanged pod template, so the # new images only land on a restart (pullPolicy is already Always). - name: Roll the services onto the new images run: | set -euo pipefail svcs="acl domain bff event-subscriber projection-api self-service openbaar behandel beheer" kubectl -n big rollout restart deploy $svcs kubectl -n big rollout status --timeout=300s deploy $svcs # Proves portal → Caddy → BFF → projection end to end. An empty register is # a pass; a 502 or a timeout is not. - name: Smoke the public register run: curl -fsS --retry 10 --retry-delay 6 --retry-all-errors http://localhost:30141/openbaar/register # Cluster-wide, not just `big`: the first thing that can fail is the registry # in its own namespace, and a scheduling problem shows up in the events, not # in `rollout status` — which only ever says "timed out waiting". - name: Pods and events on failure if: failure() run: | kubectl get pods -A -o wide || true kubectl -n big get jobs || true kubectl get events -A --sort-by=.lastTimestamp | tail -30 || true