Public-HTTPS reliability + diagnostics + multi-cluster UX (2026-05-28, follow-up)¶
Motivation¶
After the 2026-05-28 incident the operator triage took ~30 minutes because the failure mode was invisible to the SPA (the Redis cache went cold after a revision restart, the SPA banner showed only "Task failed (phase=wait_certificate_ready)", and the running task progress was forgotten when the operator switched away from the Public HTTPS tab). This change ships ten follow-up improvements grouped into three waves (A operability, B safety, C polish).
User-facing change¶
- Public HTTPS status survives revision restarts. The Redis-only
cache is now backed by a durable Storage Table singleton, and a new
beat task (
api.tasks.openapi.reconcile_public_https, default every 120s) refreshes the hot cache from the singleton + the live Certificate state.cert_expires_atis now populated automatically. - Setup failure surfaces the real root cause. Failed
setup_openapi_public_httpstasks now ship adiagnosticsfield collected from Certificate / Order / Challenge / solver-Pod state (certificate.condition,order.state,challenge.reason,challenge.solverpod). The SPA renders the multi-line digest under the error banner so the operator can tell "wrong status code '503'" apart from "invalidContact" withoutkubectl describe. - Cross-subscription deploy guard.
quick-deploy.shnow hard-fails with a red ERROR when the activeaz loginsubscription differs from theazd envsubscription, requiringELB_ALLOW_SUB_MISMATCH=1to proceed. Stops the "deployed to the wrong cluster" class of mistake that happened on 2026-05-28. - SPA Operator-email validator stays in sync with the backend. The
panel fetches
/api/aks/openapi/public-https/operator-email-ruleson mount and unions the server'sprivate_use_tldslist with the hard-coded fallback. Adding a TLD to_PRIVATE_USE_TLDSon the backend now propagates to the SPA without a new build. - Public HTTPS progress survives tab switches across multiple
clusters. localStorage key is now
elb.publicHttps.runningTask.v2.<cluster>with a legacyv1migrator. Two clusters can be enabled in parallel without each other's progress badge being clobbered. - Systempool capacity early warning. New
/api/monitor/aks/node-pressurereports per-pool CPU/memory request % with a 90% warning flag. SPA hookup will land in a follow-up PR.
API / IaC diff summary¶
api/services/state/singletons.py(new) — genericsave_singleton/load_singleton/clear_singletonbacked by an Azure Tabledashboardsingletons. Endpoint env:AZURE_TABLE_ENDPOINT.api/services/openapi/runtime.py—save_openapi_public_base_urlnow dual-writes (durable + Redis);get_openapi_public_base_urlreads Redis first then falls back to the durable singleton and re-populates Redis opportunistically;clear_openapi_public_base_urlclears both tiers.api/tasks/openapi/reconcile_public_https.py(new) — Celery beat taskapi.tasks.openapi.reconcile_public_https. Beat schedule: 120s (envCELERY_BEAT_OPENAPI_PUBLIC_HTTPS_SECONDS).api/tasks/openapi/public_https.py— new_collect_cert_issuance_diagnostics(...)helper invoked from_wait_for_certificate_readyAND the pipeline-levelexcept; the failed task result now carriesdiagnostics(≤2000 chars).api/services/k8s/ingress.py—build_cluster_issuernow usesworkload_pool_pod_template()so future ACME Issuers reuse the same blastpool-friendly podTemplate without copy-paste. LegacySYSTEM_POOL_*/*_for_system_poolaliases removed in the same change; the canonical names areWORKLOAD_POOL_*/patch_manifest_for_workload_pool/fetch_install_manifest_for_workload_pool.api/routes/aks/openapi.py— newGET /api/aks/openapi/public-https/operator-email-rulesexposes the validator rules (private TLDs, regex, max length) for the SPA to mirror.api/routes/monitor/aks.py— newGET /api/monitor/aks/node-pressureusingapi/services/k8s/node_pressure.py::k8s_node_request_pressure.scripts/dev/az-context.sh—prepare_deploy_env_from_az_loginfails fast (exit 2) on sub mismatch unlessELB_ALLOW_SUB_MISMATCH=1is set.web/src/components/SettingsPanel.tsx— multi-cluster localStorage (v2.<cluster>), server-synced private-TLD set, diagnostics surfacing under the error banner,StatusLinenow useswhite-space: pre-wrapso multi-line errors render correctly.web/src/api/aks.ts— newopenApiOperatorEmailRules()typed client.
No Bicep changes (the durable table is created lazily on first write;
AZURE_TABLE_ENDPOINT is already injected on the api / worker sidecars).
Validation evidence¶
uv run pytest -q api/tests→ 1758 passed, 3 skipped (was 1562 before; +196 new tests across state singletons, reconcile, node-pressure, cert-manager challenge, ingress patch rename).uv run ruff check api/...for every changed module → All checks passed.cd web && npm run build→ built in 9.52s, no errors.cd web && npx vitest run→ 53 files / 394 tests passed.
Files touched (this follow-up only)¶
api/services/state/singletons.py (new)
api/services/openapi/runtime.py
api/services/k8s/ingress.py
api/services/k8s/node_pressure.py (new)
api/tasks/openapi/__init__.py
api/tasks/openapi/public_https.py
api/tasks/openapi/reconcile_public_https.py (new)
api/celery_app.py
api/routes/monitor/aks.py
api/routes/aks/openapi.py
api/tests/test_state_singletons.py (new)
api/tests/test_openapi_public_https_reconcile.py (new)
api/tests/test_k8s_node_pressure.py (new)
api/tests/test_cert_manager_challenge_path.py (new)
api/tests/test_openapi_public_https.py (rename + assertions)
scripts/dev/az-context.sh
web/src/components/SettingsPanel.tsx
web/src/api/aks.ts
Follow-up parked¶
- SPA card consuming
/api/monitor/aks/node-pressure(UI design TBD). cert-manager-level admission policy that injects the workload-pool podTemplate on every solver Pod (Kyverno or webhook) — would let us drop the per-Issuer podTemplate plumbing entirely.