Files
cc-ci/nix/modules/dashboard.nix

104 lines
4.3 KiB
Nix

# Results dashboard (§4.5, D7): the YunoHost-CI-like overview at ci.commoninternet.net. Reads the
# Drone API (read-only) and renders latest-run-per-recipe + SVG badges. Packaged as a Nix-built OCI
# image and run as a swarm service on `proxy`, routed by traefik at Host(ci.commoninternet.net) — the
# comment-bridge's Host && PathPrefix(`/hook`) rule is longer, so /hook still wins (priority by rule
# length). Deployed by an idempotent-reconcile oneshot (same pattern as bridge/drone).
{ pkgs, ... }:
let
dashApp = pkgs.runCommand "cc-ci-dashboard-app" { } ''
mkdir -p $out/app
cp ${../../dashboard/dashboard.py} $out/app/dashboard.py
'';
# Content-derived tag: changes whenever dashboard.py changes, so `docker stack deploy` actually
# rolls the service to the new image (a fixed `:latest` tag + unchanged stack spec does NOT roll —
# swarm sees no change). Reproducible + self-healing.
imageTag = builtins.substring 0 12 (builtins.hashString "sha256"
(builtins.readFile ../../dashboard/dashboard.py));
image = pkgs.dockerTools.buildLayeredImage {
name = "cc-ci-dashboard";
tag = imageTag;
contents = [ pkgs.python3 pkgs.cacert dashApp ];
config = {
Cmd = [ "${pkgs.python3}/bin/python3" "/app/dashboard.py" ];
Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ];
ExposedPorts = { "8080/tcp" = { }; };
};
};
stack = pkgs.writeText "cc-ci-dashboard-stack.yml" ''
version: "3.8"
services:
app:
image: cc-ci-dashboard:${imageTag}
environment:
- DRONE_URL=https://drone.ci.commoninternet.net
- CI_REPO=recipe-maintainers/cc-ci
- DASH_LISTEN=0.0.0.0:8080
- DRONE_TOKEN_FILE=/run/secrets/drone_token
- CCCI_RUNS_DIR=/var/lib/cc-ci-runs
secrets:
- drone_token
# Phase 3 (U2.3): the per-run artifacts (results.json, summary.png, screenshot.png, badge.svg)
# the runner writes under /var/lib/cc-ci-runs are bind-mounted READ-ONLY so the dashboard can
# serve them at /runs/<id>/<file>. Read-only: the dashboard never writes run artifacts.
volumes:
- type: bind
source: /var/lib/cc-ci-runs
target: /var/lib/cc-ci-runs
read_only: true
networks:
- proxy
deploy:
replicas: 1
restart_policy:
condition: any
labels:
- "traefik.enable=true"
- "traefik.http.services.ccci-dashboard.loadbalancer.server.port=8080"
- "traefik.http.routers.ccci-dashboard.rule=Host(`ci.commoninternet.net`)"
- "traefik.http.routers.ccci-dashboard.entrypoints=web-secure"
- "traefik.http.routers.ccci-dashboard.tls=true"
networks:
proxy:
external: true
secrets:
drone_token:
external: true
name: cc_ci_dashboard_drone_token_v1
'';
reconcile = pkgs.writeShellApplication {
name = "cc-ci-reconcile-dashboard";
runtimeInputs = with pkgs; [ docker coreutils ];
text = ''
if [ ! -r /run/secrets/bridge_drone_token ]; then
echo "FATAL: /run/secrets/bridge_drone_token missing (rebuild ordering?)" >&2
exit 1
fi
docker load -i ${image}
# Dashboard reads the Drone API read-only; reuse the same Drone token value as the bridge.
docker secret inspect cc_ci_dashboard_drone_token_v1 >/dev/null 2>&1 \
|| docker secret create cc_ci_dashboard_drone_token_v1 /run/secrets/bridge_drone_token >/dev/null
docker stack deploy --detach=true -c ${stack} ccci-dashboard
'';
};
in
{
systemd.services.deploy-dashboard = {
description = "Reconcile the cc-ci results dashboard (overview + badges) swarm service";
# Serialized after deploy-bridge (chain proxy→drone→bridge→dashboard→backupbot) to avoid the
# concurrent abra-init race on a fresh host (see bridge.nix). Ordering-only.
after = [ "deploy-bridge.service" "deploy-proxy.service" "swarm-init.service" "docker.service" "network-online.target" ];
requires = [ "swarm-init.service" "docker.service" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${reconcile}/bin/cc-ci-reconcile-dashboard";
};
};
}