#!/usr/bin/env bash # Run the full e2e suite against the LOCAL k3d/Tilt stack. # # orchestration-api is intentionally NOT deployed to k8s — it runs here as a # separate local process (per design). Everything else (yucca-api, michael, # mock-oidc, web, db, Ceph) comes from the cluster via kubectl port-forwards. # # No /etc/hosts / sudo required: # - the jest suite resolves the in-cluster OIDC issuer host via a dns preload # (hostmap.cjs); the playwright browser via --host-resolver-rules. # - yucca-api's topology must advertise a localhost rest_url, because restic # runs on this host (the cluster keeps the in-cluster name for real use). # `tilt up` with YUCCA_TOPOLOGY_LOCAL_REST=1 renders that variant up front; # otherwise we patch the ConfigMap and restart yucca-api here. Either way # the orchestrator discovers everything through the meta pod's well-known # pointer, exercising the real discovery chain. # # Prereq: mise k3d:up && mise tilt:up (cluster healthy, context k3d-yucca) set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/../../.." && pwd)" cd "$ROOT" [ "$(kubectl config current-context)" = "k3d-yucca" ] || { echo "Not on k3d-yucca. Run: mise k3d:up && mise tilt:up" >&2; exit 1; } PF_PIDS=() ORCH_PGID="" cleanup() { echo "==> cleanup" # Kill only what WE started (a broad pkill would take out the user's own # port-forwards). Signal the orchestrator's whole process group: it is a # mise->pnpm->ts-node tree, and killing just the head orphans the node process # holding :22676, which then breaks the next run. [ -n "$ORCH_PGID" ] && kill -- "-$ORCH_PGID" 2>/dev/null || true for p in "${PF_PIDS[@]:-}"; do kill "$p" 2>/dev/null || true; done # Restore the in-cluster rest_url in the topology. If this run is SIGKILLed # the override survives — the next run reapplies + reverts it. if [ -n "${ORIG_TOPOLOGY:-}" ]; then kubectl -n yucca create configmap yucca-topology --from-literal=topology.json="$ORIG_TOPOLOGY" \ --dry-run=client -o yaml | kubectl apply -f - >/dev/null 2>&1 || true kubectl -n yucca rollout restart deploy/yucca-api >/dev/null 2>&1 || true fi } trap cleanup EXIT # CI has already built the workspace by this point. if [ -z "${YUCCA_E2E_PREBUILT:-}" ]; then echo "==> build workspace libs (sdk consumed by orchestration-api + the e2e)" mise run //:common:build >/dev/null mise run //packages/yucca-sdk/orchestration-ui:build >/dev/null fi echo "==> build restic-proxy (spawned by orchestration-api and the proxy suites)" mise run //packages/restic-proxy:build >/dev/null export PATH="$ROOT/dist:$PATH" echo "==> port-forward k3d services to the e2e host ports" kubectl port-forward -n yucca svc/yucca-michael 3010:3010 >/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) kubectl port-forward -n yucca svc/yucca-mock-oidc 8092:8092 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) kubectl port-forward -n yucca svc/yucca-meta 8080:8080 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) # web on :36033 (orchestration-api + the web e2e target) AND :5173 (yucca-api's # OIDC redirect_uri, which the OIDC callback follows). kubectl port-forward -n yucca svc/yucca-web 36033:5173 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) kubectl port-forward -n yucca svc/yucca-web 5173:5173 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) # victoria-logs (:9428) so the telemetry e2e can read back structured logs that # yucca-api ships via OTLP. kubectl port-forward -n yucca svc/victoria-logs 9428:9428 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) TOPOLOGY="$(kubectl -n yucca get configmap yucca-topology -o jsonpath='{.data.topology\.json}')" case "$TOPOLOGY" in *yucca-michael*) echo "==> e2e-only: topology rest_url -> localhost (restic runs on this host)" ORIG_TOPOLOGY="$TOPOLOGY" kubectl -n yucca create configmap yucca-topology \ --from-literal=topology.json="${ORIG_TOPOLOGY//yucca-michael/localhost}" \ --dry-run=client -o yaml | kubectl apply -f - >/dev/null kubectl -n yucca rollout restart deploy/yucca-api >/dev/null kubectl -n yucca rollout status deploy/yucca-api --timeout=120s >/dev/null ;; *) echo "==> topology already advertises a localhost rest_url — no yucca-api restart needed" ;; esac # After the case, never before: on the patching path the restart above replaces # the pod, and a forward opened earlier dies with it. kubectl port-forward -n yucca svc/yucca-api 3020:3020 >>/tmp/yucca-e2e-pf.log 2>&1 & PF_PIDS+=($!) if (exec 3<>/dev/tcp/localhost/22676) 2>/dev/null; then echo "Something is already listening on :22676 — a leftover orchestration-api?" >&2 echo "The one we start would fail to bind and the suites would fail on it." >&2 exit 1 fi echo "==> launch orchestration-api as a separate local process (:22676)" # Discovery runs the real chain: meta pod well-known -> /api/meta -> api_root. # setsid gives it a process group cleanup can reap whole. FUTO_BACKUPS_WELL_KNOWN_URL="http://localhost:8080/.well-known/yucca.json" \ NODE_OPTIONS="--require $HERE/hostmap.cjs" \ setsid mise run //packages/yucca-sdk/orchestration-api:dev >/tmp/yucca-e2e-orch.log 2>&1 & ORCH_PGID="$(ps -o pgid= -p $! 2>/dev/null | tr -d ' ')" wait_for_http() { local name="$1" url="$2" want="$3" for _ in $(seq 1 90); do [ "$(curl -s -o /dev/null -w '%{http_code}' -m2 "$url" 2>/dev/null)" = "$want" ] && return 0 sleep 1 done echo "timed out waiting for $name at $url" >&2 tail -40 /tmp/yucca-e2e-orch.log >&2 return 1 } wait_for_http yucca-api http://localhost:3020/api/meta 200 # Onboarding status, not just any response: it is the first thing the suites # call, and only answers once discovery has reached a live yucca-api. wait_for_http orchestration-api http://localhost:22676/api/yucca/onboarding 200 echo "==> jest e2e (it-works, restic, yucca-api, orchestration-api)" # shellcheck disable=SC1091 source .mise/tasks/michael/env # shellcheck disable=SC1091 source packages/yucca-api/.mise/tasks/env # Three, not one per suite: orchestration-api alone runs ~105s and gates the # phase, so a fourth worker adds no wall-clock — only a fourth CPU consumer on a # runner that is already hosting the cluster these suites drive. NODE_OPTIONS="--experimental-vm-modules --require $HERE/hostmap.cjs" \ pnpm --filter e2e exec jest --maxWorkers=3 grep -q "Minted a new token" /tmp/yucca-e2e-orch.log || { echo "orchestration-api never routed restic through restic-proxy" >&2; exit 1; } echo "==> web e2e (playwright against the k3d web)" # Config lives in packages/web so @playwright/test resolves from web's deps. pnpm --filter web exec playwright test --config="$ROOT/packages/web/playwright.k3d.config.ts" echo "==> ALL E2E PASSED against local k8s"