From a8e84b4011afec482def5775b776b0f24a30d412 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Aug 2026 10:27:58 +0100 Subject: [PATCH] Bound legacy demo bootstrap workload --- .github/scripts/recover-demo-runtime.sh | 67 +++++++++++++++++-- .../scripts/resolve-demo-runtime-profile.sh | 42 +++++++++--- .github/workflows/recover-demo-server.yml | 15 ++++- .github/workflows/update-demo-server.yml | 53 +++++++++++---- .../subsystems/deployment-installability.md | 28 ++++---- scripts/demo_public_browser_smoke.cjs | 18 +++++ .../installtests/build_release_assets_test.go | 27 ++++++-- .../release_promotion_policy_test.py | 29 +++++++- 8 files changed, 232 insertions(+), 47 deletions(-) diff --git a/.github/scripts/recover-demo-runtime.sh b/.github/scripts/recover-demo-runtime.sh index 2abc4e2ce3..5ca75b5a05 100755 --- a/.github/scripts/recover-demo-runtime.sh +++ b/.github/scripts/recover-demo-runtime.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -if [ "$#" -ne 6 ]; then - echo "usage: recover-demo-runtime.sh EXPECTED_HOSTNAME LOCAL_BASE_URL EXPECTED_VERSION MOCK_NODES MOCK_SEED_DURATION MOCK_UPDATE_INTERVAL" >&2 +if [ "$#" -ne 15 ]; then + echo "usage: recover-demo-runtime.sh EXPECTED_HOSTNAME LOCAL_BASE_URL EXPECTED_VERSION MOCK_NODES MOCK_VMS_PER_NODE MOCK_LXCS_PER_NODE MOCK_DOCKER_HOSTS MOCK_DOCKER_CONTAINERS MOCK_GENERIC_HOSTS MOCK_K8S_CLUSTERS MOCK_K8S_NODES MOCK_K8S_PODS MOCK_K8S_DEPLOYMENTS MOCK_SEED_DURATION MOCK_UPDATE_INTERVAL" >&2 exit 2 fi @@ -10,8 +10,17 @@ EXPECTED_HOSTNAME="$1" LOCAL_BASE_URL="${2%/}" EXPECTED_VERSION="${3#v}" MOCK_NODES="$4" -MOCK_SEED_DURATION="$5" -MOCK_UPDATE_INTERVAL="$6" +MOCK_VMS_PER_NODE="$5" +MOCK_LXCS_PER_NODE="$6" +MOCK_DOCKER_HOSTS="$7" +MOCK_DOCKER_CONTAINERS="$8" +MOCK_GENERIC_HOSTS="$9" +MOCK_K8S_CLUSTERS="${10}" +MOCK_K8S_NODES="${11}" +MOCK_K8S_PODS="${12}" +MOCK_K8S_DEPLOYMENTS="${13}" +MOCK_SEED_DURATION="${14}" +MOCK_UPDATE_INTERVAL="${15}" SERVICE_NAME="pulse" RELAY_SERVICE_NAME="pulse-relay" EXPECTED_BINARY="/opt/pulse/bin/pulse" @@ -19,7 +28,19 @@ EXPECTED_UNIT="/etc/systemd/system/pulse.service" MUTATED=false BACKUP_DIR="" -[[ "$MOCK_NODES" =~ ^[1-9][0-9]*$ ]] +for fixture_count in \ + "$MOCK_NODES" \ + "$MOCK_VMS_PER_NODE" \ + "$MOCK_LXCS_PER_NODE" \ + "$MOCK_DOCKER_HOSTS" \ + "$MOCK_DOCKER_CONTAINERS" \ + "$MOCK_GENERIC_HOSTS" \ + "$MOCK_K8S_CLUSTERS" \ + "$MOCK_K8S_NODES" \ + "$MOCK_K8S_PODS" \ + "$MOCK_K8S_DEPLOYMENTS"; do + [[ "$fixture_count" =~ ^[1-9][0-9]*$ ]] +done [[ "$MOCK_SEED_DURATION" =~ ^[1-9][0-9]*[smhd]$ ]] [[ "$MOCK_UPDATE_INTERVAL" =~ ^[1-9][0-9]*[smh]$ ]] @@ -71,6 +92,15 @@ set_env_value() { runtime_profile_matches() { sudo grep -Fxq "PULSE_MOCK_NODES=${MOCK_NODES}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_VMS_PER_NODE=${MOCK_VMS_PER_NODE}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_LXCS_PER_NODE=${MOCK_LXCS_PER_NODE}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_DOCKER_HOSTS=${MOCK_DOCKER_HOSTS}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_DOCKER_CONTAINERS=${MOCK_DOCKER_CONTAINERS}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_GENERIC_HOSTS=${MOCK_GENERIC_HOSTS}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_K8S_CLUSTERS=${MOCK_K8S_CLUSTERS}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_K8S_NODES=${MOCK_K8S_NODES}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_K8S_PODS=${MOCK_K8S_PODS}" /etc/pulse/.env && + sudo grep -Fxq "PULSE_MOCK_K8S_DEPLOYMENTS=${MOCK_K8S_DEPLOYMENTS}" /etc/pulse/.env && sudo grep -Fxq "PULSE_MOCK_TRENDS_SEED_DURATION=${MOCK_SEED_DURATION}" /etc/pulse/.env && sudo grep -Fxq "PULSE_MOCK_UPDATE_INTERVAL=${MOCK_UPDATE_INTERVAL}" /etc/pulse/.env } @@ -137,6 +167,15 @@ emit_evidence() { --arg version "$version" \ --arg expected_version "$EXPECTED_VERSION" \ --arg mock_nodes "$MOCK_NODES" \ + --arg mock_vms_per_node "$MOCK_VMS_PER_NODE" \ + --arg mock_lxcs_per_node "$MOCK_LXCS_PER_NODE" \ + --arg mock_docker_hosts "$MOCK_DOCKER_HOSTS" \ + --arg mock_docker_containers "$MOCK_DOCKER_CONTAINERS" \ + --arg mock_generic_hosts "$MOCK_GENERIC_HOSTS" \ + --arg mock_k8s_clusters "$MOCK_K8S_CLUSTERS" \ + --arg mock_k8s_nodes "$MOCK_K8S_NODES" \ + --arg mock_k8s_pods "$MOCK_K8S_PODS" \ + --arg mock_k8s_deployments "$MOCK_K8S_DEPLOYMENTS" \ --arg mock_seed_duration "$MOCK_SEED_DURATION" \ --arg mock_update_interval "$MOCK_UPDATE_INTERVAL" \ '{schema_version: 1, status: $status, mutated: $mutated, @@ -157,6 +196,15 @@ emit_evidence() { after_runtime_config_sha256: $after_runtime_config_sha256, version: $version, expected_version: $expected_version, runtime_profile: {mock_nodes: $mock_nodes, + mock_vms_per_node: $mock_vms_per_node, + mock_lxcs_per_node: $mock_lxcs_per_node, + mock_docker_hosts: $mock_docker_hosts, + mock_docker_containers: $mock_docker_containers, + mock_generic_hosts: $mock_generic_hosts, + mock_k8s_clusters: $mock_k8s_clusters, + mock_k8s_nodes: $mock_k8s_nodes, + mock_k8s_pods: $mock_k8s_pods, + mock_k8s_deployments: $mock_k8s_deployments, mock_seed_duration: $mock_seed_duration, mock_update_interval: $mock_update_interval}}' } @@ -220,6 +268,15 @@ trap rollback_on_error ERR log "applying the target-compatible demo runtime profile and restarting only pulse.service" MUTATED=true set_env_value PULSE_MOCK_NODES "$MOCK_NODES" +set_env_value PULSE_MOCK_VMS_PER_NODE "$MOCK_VMS_PER_NODE" +set_env_value PULSE_MOCK_LXCS_PER_NODE "$MOCK_LXCS_PER_NODE" +set_env_value PULSE_MOCK_DOCKER_HOSTS "$MOCK_DOCKER_HOSTS" +set_env_value PULSE_MOCK_DOCKER_CONTAINERS "$MOCK_DOCKER_CONTAINERS" +set_env_value PULSE_MOCK_GENERIC_HOSTS "$MOCK_GENERIC_HOSTS" +set_env_value PULSE_MOCK_K8S_CLUSTERS "$MOCK_K8S_CLUSTERS" +set_env_value PULSE_MOCK_K8S_NODES "$MOCK_K8S_NODES" +set_env_value PULSE_MOCK_K8S_PODS "$MOCK_K8S_PODS" +set_env_value PULSE_MOCK_K8S_DEPLOYMENTS "$MOCK_K8S_DEPLOYMENTS" set_env_value PULSE_MOCK_TRENDS_SEED_DURATION "$MOCK_SEED_DURATION" set_env_value PULSE_MOCK_UPDATE_INTERVAL "$MOCK_UPDATE_INTERVAL" sudo chown pulse:pulse /etc/pulse/.env diff --git a/.github/scripts/resolve-demo-runtime-profile.sh b/.github/scripts/resolve-demo-runtime-profile.sh index 598d84bbc4..790a7695be 100755 --- a/.github/scripts/resolve-demo-runtime-profile.sh +++ b/.github/scripts/resolve-demo-runtime-profile.sh @@ -19,21 +19,47 @@ if git grep -q 'mockEagerHistoryPVEGuestLimit' "${TAG_REF}" -- internal/monitori git grep -q 'UpdateMetricCohort' "${TAG_REF}" -- internal/mock/integration.go; then PROFILE="large-estate" MOCK_NODES=50 + MOCK_VMS_PER_NODE=10 + MOCK_LXCS_PER_NODE=8 + MOCK_DOCKER_HOSTS=5 + MOCK_DOCKER_CONTAINERS=14 + MOCK_GENERIC_HOSTS=4 + MOCK_K8S_CLUSTERS=3 + MOCK_K8S_NODES=5 + MOCK_K8S_PODS=40 + MOCK_K8S_DEPLOYMENTS=14 MOCK_SEED_DURATION=48h MOCK_UPDATE_INTERVAL=2s else PROFILE="legacy-bounded" - MOCK_NODES="$({ - git show "${TAG_REF}:.github/workflows/update-demo-server.yml" 2>/dev/null || true - } | sed -n -E 's/^[[:space:]]*set_env_value PULSE_MOCK_NODES ([0-9]+)[[:space:]]*$/\1/p' | tail -1)" - if [[ ! "${MOCK_NODES}" =~ ^[1-9][0-9]*$ ]]; then - MOCK_NODES=32 - fi - MOCK_SEED_DURATION=6h - MOCK_UPDATE_INTERVAL=10s + # Older runtimes update every fixture on every tick and eagerly retain every + # history series. Keep their public-demo payload deliberately compact; the + # tagged workflow's former 32-node default still produced a 4 MB state + # bootstrap and could leave real browsers parked on the loading shell. + MOCK_NODES=8 + MOCK_VMS_PER_NODE=6 + MOCK_LXCS_PER_NODE=4 + MOCK_DOCKER_HOSTS=2 + MOCK_DOCKER_CONTAINERS=8 + MOCK_GENERIC_HOSTS=2 + MOCK_K8S_CLUSTERS=1 + MOCK_K8S_NODES=3 + MOCK_K8S_PODS=12 + MOCK_K8S_DEPLOYMENTS=4 + MOCK_SEED_DURATION=2h + MOCK_UPDATE_INTERVAL=15s fi printf 'profile=%s\n' "$PROFILE" printf 'mock_nodes=%s\n' "$MOCK_NODES" +printf 'mock_vms_per_node=%s\n' "$MOCK_VMS_PER_NODE" +printf 'mock_lxcs_per_node=%s\n' "$MOCK_LXCS_PER_NODE" +printf 'mock_docker_hosts=%s\n' "$MOCK_DOCKER_HOSTS" +printf 'mock_docker_containers=%s\n' "$MOCK_DOCKER_CONTAINERS" +printf 'mock_generic_hosts=%s\n' "$MOCK_GENERIC_HOSTS" +printf 'mock_k8s_clusters=%s\n' "$MOCK_K8S_CLUSTERS" +printf 'mock_k8s_nodes=%s\n' "$MOCK_K8S_NODES" +printf 'mock_k8s_pods=%s\n' "$MOCK_K8S_PODS" +printf 'mock_k8s_deployments=%s\n' "$MOCK_K8S_DEPLOYMENTS" printf 'mock_seed_duration=%s\n' "$MOCK_SEED_DURATION" printf 'mock_update_interval=%s\n' "$MOCK_UPDATE_INTERVAL" diff --git a/.github/workflows/recover-demo-server.yml b/.github/workflows/recover-demo-server.yml index 7536750b79..7344cfbe81 100644 --- a/.github/workflows/recover-demo-server.yml +++ b/.github/workflows/recover-demo-server.yml @@ -18,6 +18,8 @@ jobs: DEMO_EXPECTED_HOSTNAME: ${{ vars.DEMO_EXPECTED_HOSTNAME }} DEMO_LOCAL_BASE_URL: ${{ vars.DEMO_LOCAL_BASE_URL }} DEMO_PUBLIC_HEALTH_URL: ${{ vars.DEMO_PUBLIC_HEALTH_URL }} + DEMO_AUTH_USER: ${{ vars.DEMO_AUTH_USER }} + DEMO_AUTH_PASS: ${{ vars.DEMO_AUTH_PASS }} steps: - name: Checkout exact recovery implementation @@ -93,6 +95,15 @@ jobs: DEMO_SERVER_USER: ${{ secrets.DEMO_SERVER_USER }} EXPECTED_VERSION: ${{ steps.stable.outputs.tag }} MOCK_NODES: ${{ steps.runtime_profile.outputs.mock_nodes }} + MOCK_VMS_PER_NODE: ${{ steps.runtime_profile.outputs.mock_vms_per_node }} + MOCK_LXCS_PER_NODE: ${{ steps.runtime_profile.outputs.mock_lxcs_per_node }} + MOCK_DOCKER_HOSTS: ${{ steps.runtime_profile.outputs.mock_docker_hosts }} + MOCK_DOCKER_CONTAINERS: ${{ steps.runtime_profile.outputs.mock_docker_containers }} + MOCK_GENERIC_HOSTS: ${{ steps.runtime_profile.outputs.mock_generic_hosts }} + MOCK_K8S_CLUSTERS: ${{ steps.runtime_profile.outputs.mock_k8s_clusters }} + MOCK_K8S_NODES: ${{ steps.runtime_profile.outputs.mock_k8s_nodes }} + MOCK_K8S_PODS: ${{ steps.runtime_profile.outputs.mock_k8s_pods }} + MOCK_K8S_DEPLOYMENTS: ${{ steps.runtime_profile.outputs.mock_k8s_deployments }} MOCK_SEED_DURATION: ${{ steps.runtime_profile.outputs.mock_seed_duration }} MOCK_UPDATE_INTERVAL: ${{ steps.runtime_profile.outputs.mock_update_interval }} run: | @@ -102,7 +113,7 @@ jobs: -o StrictHostKeyChecking=yes \ -o UserKnownHostsFile=~/.ssh/known_hosts \ "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \ - "bash -s -- $(printf '%q ' "$DEMO_EXPECTED_HOSTNAME" "$DEMO_LOCAL_BASE_URL" "$EXPECTED_VERSION" "$MOCK_NODES" "$MOCK_SEED_DURATION" "$MOCK_UPDATE_INTERVAL")" \ + "bash -s -- $(printf '%q ' "$DEMO_EXPECTED_HOSTNAME" "$DEMO_LOCAL_BASE_URL" "$EXPECTED_VERSION" "$MOCK_NODES" "$MOCK_VMS_PER_NODE" "$MOCK_LXCS_PER_NODE" "$MOCK_DOCKER_HOSTS" "$MOCK_DOCKER_CONTAINERS" "$MOCK_GENERIC_HOSTS" "$MOCK_K8S_CLUSTERS" "$MOCK_K8S_NODES" "$MOCK_K8S_PODS" "$MOCK_K8S_DEPLOYMENTS" "$MOCK_SEED_DURATION" "$MOCK_UPDATE_INTERVAL")" \ < .github/scripts/recover-demo-runtime.sh \ > demo-runtime-recovery.json jq -e '.schema_version == 1 and (.status == "recovered" or .status == "healthy_noop")' \ @@ -139,6 +150,8 @@ jobs: run: | set -euo pipefail export PULSE_PUBLIC_SITE_URL="${DEMO_PUBLIC_HEALTH_URL%/api/health}/" + export PULSE_DEMO_AUTH_USER="${DEMO_AUTH_USER:-demo}" + export PULSE_DEMO_AUTH_PASS="${DEMO_AUTH_PASS:-demo}" ./scripts/run_demo_public_browser_smoke.sh - name: Compensate failed mutated recovery diff --git a/.github/workflows/update-demo-server.yml b/.github/workflows/update-demo-server.yml index a6dc42c725..b3da987961 100644 --- a/.github/workflows/update-demo-server.yml +++ b/.github/workflows/update-demo-server.yml @@ -50,6 +50,15 @@ jobs: environment_name: ${{ steps.target.outputs.environment_name }} skip: ${{ steps.target.outputs.skip || 'false' }} mock_nodes: ${{ steps.runtime_profile.outputs.mock_nodes }} + mock_vms_per_node: ${{ steps.runtime_profile.outputs.mock_vms_per_node }} + mock_lxcs_per_node: ${{ steps.runtime_profile.outputs.mock_lxcs_per_node }} + mock_docker_hosts: ${{ steps.runtime_profile.outputs.mock_docker_hosts }} + mock_docker_containers: ${{ steps.runtime_profile.outputs.mock_docker_containers }} + mock_generic_hosts: ${{ steps.runtime_profile.outputs.mock_generic_hosts }} + mock_k8s_clusters: ${{ steps.runtime_profile.outputs.mock_k8s_clusters }} + mock_k8s_nodes: ${{ steps.runtime_profile.outputs.mock_k8s_nodes }} + mock_k8s_pods: ${{ steps.runtime_profile.outputs.mock_k8s_pods }} + mock_k8s_deployments: ${{ steps.runtime_profile.outputs.mock_k8s_deployments }} mock_seed_duration: ${{ steps.runtime_profile.outputs.mock_seed_duration }} mock_update_interval: ${{ steps.runtime_profile.outputs.mock_update_interval }} @@ -545,6 +554,15 @@ jobs: set -euo pipefail SERVICE_NAME="${{ steps.config.outputs.service_name }}" MOCK_NODES="${{ needs.resolve.outputs.mock_nodes }}" + MOCK_VMS_PER_NODE="${{ needs.resolve.outputs.mock_vms_per_node }}" + MOCK_LXCS_PER_NODE="${{ needs.resolve.outputs.mock_lxcs_per_node }}" + MOCK_DOCKER_HOSTS="${{ needs.resolve.outputs.mock_docker_hosts }}" + MOCK_DOCKER_CONTAINERS="${{ needs.resolve.outputs.mock_docker_containers }}" + MOCK_GENERIC_HOSTS="${{ needs.resolve.outputs.mock_generic_hosts }}" + MOCK_K8S_CLUSTERS="${{ needs.resolve.outputs.mock_k8s_clusters }}" + MOCK_K8S_NODES="${{ needs.resolve.outputs.mock_k8s_nodes }}" + MOCK_K8S_PODS="${{ needs.resolve.outputs.mock_k8s_pods }}" + MOCK_K8S_DEPLOYMENTS="${{ needs.resolve.outputs.mock_k8s_deployments }}" MOCK_SEED_DURATION="${{ needs.resolve.outputs.mock_seed_duration }}" MOCK_UPDATE_INTERVAL="${{ needs.resolve.outputs.mock_update_interval }}" REMOTE_SCRIPT=$(cat <<'EOF' @@ -552,8 +570,17 @@ jobs: SERVICE_NAME="${1:-pulse}" DEMO_LOCAL_BASE_URL="$2" MOCK_NODES="$3" - MOCK_SEED_DURATION="$4" - MOCK_UPDATE_INTERVAL="$5" + MOCK_VMS_PER_NODE="$4" + MOCK_LXCS_PER_NODE="$5" + MOCK_DOCKER_HOSTS="$6" + MOCK_DOCKER_CONTAINERS="$7" + MOCK_GENERIC_HOSTS="$8" + MOCK_K8S_CLUSTERS="$9" + MOCK_K8S_NODES="${10}" + MOCK_K8S_PODS="${11}" + MOCK_K8S_DEPLOYMENTS="${12}" + MOCK_SEED_DURATION="${13}" + MOCK_UPDATE_INTERVAL="${14}" CONFIG_DIR="/etc/${SERVICE_NAME:-pulse}" SERVICE_USER="$(systemctl show "$SERVICE_NAME" --property=User --value 2>/dev/null || true)" SERVICE_GROUP="$(systemctl show "$SERVICE_NAME" --property=Group --value 2>/dev/null || true)" @@ -636,15 +663,15 @@ jobs: set_env_value DEMO_MODE true set_env_value PULSE_MOCK_MODE true set_env_value PULSE_MOCK_NODES "$MOCK_NODES" - set_env_value PULSE_MOCK_VMS_PER_NODE 10 - set_env_value PULSE_MOCK_LXCS_PER_NODE 8 - set_env_value PULSE_MOCK_DOCKER_HOSTS 5 - set_env_value PULSE_MOCK_DOCKER_CONTAINERS 14 - set_env_value PULSE_MOCK_GENERIC_HOSTS 4 - set_env_value PULSE_MOCK_K8S_CLUSTERS 3 - set_env_value PULSE_MOCK_K8S_NODES 5 - set_env_value PULSE_MOCK_K8S_PODS 40 - set_env_value PULSE_MOCK_K8S_DEPLOYMENTS 14 + set_env_value PULSE_MOCK_VMS_PER_NODE "$MOCK_VMS_PER_NODE" + set_env_value PULSE_MOCK_LXCS_PER_NODE "$MOCK_LXCS_PER_NODE" + set_env_value PULSE_MOCK_DOCKER_HOSTS "$MOCK_DOCKER_HOSTS" + set_env_value PULSE_MOCK_DOCKER_CONTAINERS "$MOCK_DOCKER_CONTAINERS" + set_env_value PULSE_MOCK_GENERIC_HOSTS "$MOCK_GENERIC_HOSTS" + set_env_value PULSE_MOCK_K8S_CLUSTERS "$MOCK_K8S_CLUSTERS" + set_env_value PULSE_MOCK_K8S_NODES "$MOCK_K8S_NODES" + set_env_value PULSE_MOCK_K8S_PODS "$MOCK_K8S_PODS" + set_env_value PULSE_MOCK_K8S_DEPLOYMENTS "$MOCK_K8S_DEPLOYMENTS" set_env_value PULSE_MOCK_RANDOM_METRICS true set_env_value PULSE_MOCK_STOPPED_PERCENT 6 set_env_value PULSE_MOCK_TRENDS_SEED_DURATION "$MOCK_SEED_DURATION" @@ -670,7 +697,7 @@ jobs: EOF ) ssh -i ~/.ssh/id_ed25519 "$DEMO_SERVER_USER@$DEMO_SERVER_HOST" \ - "bash -s -- $(printf '%q ' "$SERVICE_NAME" "$DEMO_LOCAL_BASE_URL" "$MOCK_NODES" "$MOCK_SEED_DURATION" "$MOCK_UPDATE_INTERVAL")" <<<"$REMOTE_SCRIPT" + "bash -s -- $(printf '%q ' "$SERVICE_NAME" "$DEMO_LOCAL_BASE_URL" "$MOCK_NODES" "$MOCK_VMS_PER_NODE" "$MOCK_LXCS_PER_NODE" "$MOCK_DOCKER_HOSTS" "$MOCK_DOCKER_CONTAINERS" "$MOCK_GENERIC_HOSTS" "$MOCK_K8S_CLUSTERS" "$MOCK_K8S_NODES" "$MOCK_K8S_PODS" "$MOCK_K8S_DEPLOYMENTS" "$MOCK_SEED_DURATION" "$MOCK_UPDATE_INTERVAL")" <<<"$REMOTE_SCRIPT" - name: Verify demo runtime env: @@ -783,6 +810,8 @@ jobs: exit 1 ;; esac + export PULSE_DEMO_AUTH_USER="${DEMO_AUTH_USER:-demo}" + export PULSE_DEMO_AUTH_PASS="${DEMO_AUTH_PASS:-demo}" ./scripts/run_demo_public_browser_smoke.sh - name: Cleanup SSH material diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index fafd0ec216..ecb8e14ad1 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -2583,11 +2583,11 @@ demo shell. Health checks and entry-asset parity are necessary but not sufficient; after those checks pass, the governed helpers `scripts/run_demo_public_browser_smoke.sh` and `scripts/demo_public_browser_smoke.cjs` must exercise the public demo in a real -Chromium session and prove the login shell actually renders instead of failing -open on API-only reachability. That proof must treat the visible login controls -as the readiness signal and must not block on Playwright `networkidle`, because -the public demo shell can keep background activity alive after the page is -already usable. +Chromium session and prove both that the login shell renders and that a public +demo login reaches the connected workspace instead of failing open on API-only +reachability or remaining on the authenticated loading shell. The proof must +use visible UI state rather than Playwright `networkidle`, because the public +demo can keep background activity alive after the page is already usable. That same demo-update verification boundary also owns the canonical v6 mock runtime state contract. `.github/workflows/update-demo-server.yml` must verify mock-mode readiness through the unified `/api/state` `resources[]` collection, @@ -2742,16 +2742,18 @@ while installing an exact tagged binary, it must inspect that tag before converging the runtime environment. The 50-node profile is permitted only when the tagged runtime contains both bounded eager guest history and cohort-based metric updates. A stable tag that predates either capability must receive the -bounded compatibility profile (the estate size declared by that tag's own demo -workflow, six hours of eager history, and a ten-second full-estate update -cadence), preventing newer workflow defaults from imposing an unqualified -memory and CPU load on an older binary. Tags without a readable numeric demo -node declaration fall back to the last legacy stable baseline of 32 nodes. +bounded compatibility profile across every fixture family: 8 Proxmox nodes +with 6 VMs and 4 LXCs per node, 2 Docker hosts with 8 containers each, 2 +generic hosts, and 1 Kubernetes cluster with 3 nodes, 12 pods, and 4 +deployments. That profile retains two hours of eager history and uses a +15-second full-estate update cadence, preventing newer workflow defaults from +imposing an unqualified bootstrap, memory, and CPU load on an older binary. The manually dispatched stable-demo recovery path consumes that same resolver -and may mutate only the three bounded mock-profile values before restarting the -already-installed `pulse` service. It must retain the current stable binary, +and may mutate only the resolver-owned bounded mock-profile values before +restarting the already-installed `pulse` service. It must retain the current stable binary, unit, drop-ins, Relay process, and billing state; prove the expected hostname -and release version plus public health, frontend parity, and browser readiness; +and release version plus public health, frontend parity, and authenticated +connected-workspace browser readiness; and restore the prior runtime configuration while stopping the failed service if post-mutation validation fails. It must not install artifacts, invoke release convergence, or accept caller-selected target/version inputs. diff --git a/scripts/demo_public_browser_smoke.cjs b/scripts/demo_public_browser_smoke.cjs index 3d740bd580..43939f0147 100644 --- a/scripts/demo_public_browser_smoke.cjs +++ b/scripts/demo_public_browser_smoke.cjs @@ -1,6 +1,8 @@ const { chromium } = require('playwright'); const siteUrl = String(process.env.PULSE_PUBLIC_SITE_URL || '').trim(); +const authUser = String(process.env.PULSE_DEMO_AUTH_USER || '').trim(); +const authPass = String(process.env.PULSE_DEMO_AUTH_PASS || ''); function assert(condition, message) { if (!condition) { @@ -16,6 +18,7 @@ function normalizeText(value) { async function main() { assert(siteUrl, 'PULSE_PUBLIC_SITE_URL is required'); + assert(Boolean(authUser) === Boolean(authPass), 'PULSE_DEMO_AUTH_USER and PULSE_DEMO_AUTH_PASS must be provided together'); const browser = await chromium.launch({ headless: true }); try { @@ -42,6 +45,21 @@ async function main() { assert(bodyText.includes(expected), `Public demo body missing ${JSON.stringify(expected)}`); } + if (authUser) { + const loginStartedAt = Date.now(); + await page.getByLabel('Username').fill(authUser); + await page.getByLabel('Password').fill(authPass); + await page.getByRole('button', { name: 'Sign in to Pulse' }).click(); + + await page.locator('main').waitFor({ state: 'visible', timeout: 60000 }); + await page.getByText('Connected', { exact: true }).first().waitFor({ state: 'visible', timeout: 60000 }); + + const authenticatedBody = normalizeText(await page.locator('body').innerText()); + assert(authenticatedBody !== 'Loading...', 'Public demo remained on the authenticated loading shell'); + assert(authenticatedBody.includes('Pulse'), 'Authenticated public demo body is missing the Pulse workspace'); + console.log(`authenticated workspace became connected in ${Date.now() - loginStartedAt}ms`); + } + console.log(`public demo browser smoke passed for ${siteUrl}`); } finally { await browser.close(); diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 5a3a79f4b8..4e42341d4b 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -1808,20 +1808,31 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) { `git grep -q 'UpdateMetricCohort'`, `PROFILE="large-estate"`, `MOCK_NODES=50`, + `MOCK_VMS_PER_NODE=10`, + `MOCK_K8S_PODS=40`, `MOCK_SEED_DURATION=48h`, `MOCK_UPDATE_INTERVAL=2s`, `PROFILE="legacy-bounded"`, - `git show "${TAG_REF}:.github/workflows/update-demo-server.yml"`, - `set_env_value PULSE_MOCK_NODES ([0-9]+)`, - `if [[ ! "${MOCK_NODES}" =~ ^[1-9][0-9]*$ ]]`, - `MOCK_SEED_DURATION=6h`, - `MOCK_UPDATE_INTERVAL=10s`, + `MOCK_NODES=8`, + `MOCK_VMS_PER_NODE=6`, + `MOCK_LXCS_PER_NODE=4`, + `MOCK_DOCKER_HOSTS=2`, + `MOCK_DOCKER_CONTAINERS=8`, + `MOCK_GENERIC_HOSTS=2`, + `MOCK_K8S_CLUSTERS=1`, + `MOCK_K8S_NODES=3`, + `MOCK_K8S_PODS=12`, + `MOCK_K8S_DEPLOYMENTS=4`, + `MOCK_SEED_DURATION=2h`, + `MOCK_UPDATE_INTERVAL=15s`, `resolve_config_dir`, `set_env_value DEMO_MODE true`, `set_env_value PULSE_MOCK_MODE true`, `set_env_value PULSE_MOCK_NODES "$MOCK_NODES"`, - `set_env_value PULSE_MOCK_VMS_PER_NODE 10`, - `set_env_value PULSE_MOCK_LXCS_PER_NODE 8`, + `set_env_value PULSE_MOCK_VMS_PER_NODE "$MOCK_VMS_PER_NODE"`, + `set_env_value PULSE_MOCK_LXCS_PER_NODE "$MOCK_LXCS_PER_NODE"`, + `set_env_value PULSE_MOCK_DOCKER_HOSTS "$MOCK_DOCKER_HOSTS"`, + `set_env_value PULSE_MOCK_K8S_PODS "$MOCK_K8S_PODS"`, `set_env_value PULSE_MOCK_TRENDS_SEED_DURATION "$MOCK_SEED_DURATION"`, `set_env_value PULSE_MOCK_UPDATE_INTERVAL "$MOCK_UPDATE_INTERVAL"`, `ensure_demo_fixture_entitlement`, @@ -1838,6 +1849,8 @@ func TestUpdateDemoWorkflowUsesGovernedNetworkPath(t *testing.T) { `gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}"`, `Stable demo mutation refuses inactive or prerelease tag`, `Verify public browser smoke`, + `PULSE_DEMO_AUTH_USER`, + `PULSE_DEMO_AUTH_PASS`, `./scripts/run_demo_public_browser_smoke.sh`, } for _, needle := range required { diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index 28d539f5c1..92fdfda356 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -1881,6 +1881,18 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("mockEagerHistoryPVEGuestLimit", demo_profile) self.assertIn("UpdateMetricCohort", demo_profile) self.assertIn('PROFILE="legacy-bounded"', demo_profile) + self.assertIn("MOCK_NODES=8", demo_profile) + self.assertIn("MOCK_VMS_PER_NODE=6", demo_profile) + self.assertIn("MOCK_LXCS_PER_NODE=4", demo_profile) + self.assertIn("MOCK_DOCKER_HOSTS=2", demo_profile) + self.assertIn("MOCK_DOCKER_CONTAINERS=8", demo_profile) + self.assertIn("MOCK_GENERIC_HOSTS=2", demo_profile) + self.assertIn("MOCK_K8S_CLUSTERS=1", demo_profile) + self.assertIn("MOCK_K8S_NODES=3", demo_profile) + self.assertIn("MOCK_K8S_PODS=12", demo_profile) + self.assertIn("MOCK_K8S_DEPLOYMENTS=4", demo_profile) + self.assertIn("MOCK_SEED_DURATION=2h", demo_profile) + self.assertIn("MOCK_UPDATE_INTERVAL=15s", demo_profile) self.assertIn("resolve_config_dir", demo) self.assertIn("set_env_value DEMO_MODE true", demo) self.assertIn("set_env_value PULSE_MOCK_MODE true", demo) @@ -1986,6 +1998,12 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("getByLabel('Username').waitFor({ state: 'visible', timeout: 120000 })", demo_smoke) self.assertIn("getByLabel('Password').waitFor({ state: 'visible', timeout: 120000 })", demo_smoke) self.assertIn("getByRole('button', { name: 'Sign in to Pulse' }).waitFor({ state: 'visible', timeout: 120000 })", demo_smoke) + self.assertIn("PULSE_DEMO_AUTH_USER", demo_smoke) + self.assertIn("PULSE_DEMO_AUTH_PASS", demo_smoke) + self.assertIn("getByRole('button', { name: 'Sign in to Pulse' }).click()", demo_smoke) + self.assertIn("page.locator('main').waitFor({ state: 'visible', timeout: 60000 })", demo_smoke) + self.assertIn("getByText('Connected', { exact: true }).first().waitFor({ state: 'visible', timeout: 60000 })", demo_smoke) + self.assertIn("Public demo remained on the authenticated loading shell", demo_smoke) self.assertNotIn("waitUntil: 'networkidle'", demo_smoke) def test_stable_demo_recovery_is_fixed_current_bits_only(self) -> None: @@ -2012,12 +2030,21 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertNotIn("install.sh", workflow) self.assertNotIn("scp ", workflow) - self.assertIn('if [ "$#" -ne 6 ]', recovery) + self.assertIn('if [ "$#" -ne 15 ]', recovery) self.assertIn('SERVICE_NAME="pulse"', recovery) self.assertIn('RELAY_SERVICE_NAME="pulse-relay"', recovery) self.assertIn('EXPECTED_BINARY="/opt/pulse/bin/pulse"', recovery) self.assertIn('EXPECTED_UNIT="/etc/systemd/system/pulse.service"', recovery) self.assertIn('set_env_value PULSE_MOCK_NODES "$MOCK_NODES"', recovery) + self.assertIn('set_env_value PULSE_MOCK_VMS_PER_NODE "$MOCK_VMS_PER_NODE"', recovery) + self.assertIn('set_env_value PULSE_MOCK_LXCS_PER_NODE "$MOCK_LXCS_PER_NODE"', recovery) + self.assertIn('set_env_value PULSE_MOCK_DOCKER_HOSTS "$MOCK_DOCKER_HOSTS"', recovery) + self.assertIn('set_env_value PULSE_MOCK_DOCKER_CONTAINERS "$MOCK_DOCKER_CONTAINERS"', recovery) + self.assertIn('set_env_value PULSE_MOCK_GENERIC_HOSTS "$MOCK_GENERIC_HOSTS"', recovery) + self.assertIn('set_env_value PULSE_MOCK_K8S_CLUSTERS "$MOCK_K8S_CLUSTERS"', recovery) + self.assertIn('set_env_value PULSE_MOCK_K8S_NODES "$MOCK_K8S_NODES"', recovery) + self.assertIn('set_env_value PULSE_MOCK_K8S_PODS "$MOCK_K8S_PODS"', recovery) + self.assertIn('set_env_value PULSE_MOCK_K8S_DEPLOYMENTS "$MOCK_K8S_DEPLOYMENTS"', recovery) self.assertIn('set_env_value PULSE_MOCK_TRENDS_SEED_DURATION "$MOCK_SEED_DURATION"', recovery) self.assertIn('set_env_value PULSE_MOCK_UPDATE_INTERVAL "$MOCK_UPDATE_INTERVAL"', recovery) self.assertIn('sudo systemctl restart "$SERVICE_NAME"', recovery)