2026-05-28 — Warmup / RBAC silent-wrong-RG fail-fast hardening¶
Motivation¶
The 2026-05-28 10–11 KST incident sweep traced the auto-warmup quiet
role_summary: {status: failed} to a silent fallback chain:
api/services/auto_warmup_reconcile.pyenqueuedwarmup_databasewithout forwardingpref.storage_resource_group— kwargs were dict-spread without listing the field explicitly, so the reconciler caller dropped it.api/tasks/storage/warmup.pythen computedstorage_resource_group or resource_group, falling back to the AKS cluster RG (rg-elb-cluster) instead of the Storage RG (rg-elb-dashboard). The ARM call against the wrong RG raised aResourceNotFoundError, was swallowed by the outerexcept Exception, and the kubelet ended up with noStorage Blob Data Contributor— but the SPA showed "Cluster ready".
This is a class of bug, not one bug: identical
X_resource_group or resource_group fall-backs existed in three
other production code paths (cluster provision, "Re-assign roles"
button, OpenAPI workload-identity setup) and in the manual warmup
HTTP route. Every one of them could mask a misrouted RBAC grant.
User-facing change¶
There is no behaviour change for correctly-configured tenants
(storage_resource_group already flows end-to-end from the SPA
forms). For misconfigured tenants the dashboard now surfaces an
explicit failure with an actionable message instead of marking the
cluster "ready" while the kubelet silently lacks RBAC. The SPA's
existing degraded-banner / error toast wiring renders the new
fail-fast message verbatim.
API / IaC diff summary¶
Backend¶
api/tasks/storage/warmup.py- Top-of-function fail-fast in
warmup_databasewhenstorage_resource_groupis missing — the validation runs before the outerexcept Exceptionso it cannot be swallowed. - Inside the RBAC ensure block, the
acr_resource_group or resource_groupfall-back was replaced with an explicitRuntimeErrormirroring the storage-RG contract. api/services/auto_warmup_reconcile.py- The
send_task("api.tasks.storage.warmup_database", kwargs={…})call now listsstorage_resource_groupexplicitly betweenstorage_accountanddatabase_name, with an inline comment explaining the recurrence vector. api/routes/warmup.py- The manual
/warmup/startroute now forwardsbody.get("storage_resource_group", "")to the task so the new fail-fast guard receives the SPA's value (it was previously dropped, which would have broken every manual warmup once the guard landed). api/tasks/openapi/rbac.pysetup_workload_identitynow recordsroles_failed["StorageBlobDataContributor"]with a clear "storage_resource_group is required" message instead of falling back to the cluster RG and producing a confusing ARM 404. The function's existing "raise RuntimeError on role failure" contract carries the explicit failure up todeploy_openapi_service, which already surfaces it asstatus: failed.api/tasks/azure/provision.pyandapi/tasks/azure/rbac.py- Both call sites of
_ensure_aks_runtime_rbacdrop thestorage_resource_group or resource_groupfall-back. The callee already has its own env-based default (AZURE_RESOURCE_GROUP/STORAGE_ACCOUNT_NAME); the silent cluster-RG fall-back was overriding that default and routing the role assignment at the wrong scope. api/routes/aks/openapi.pyaks_openapi_proxy503 response now carries aRetry-After: 15HTTP header and aretry_after_seconds: 15body field so SPA consumers can back off instead of polling tight when the elb-openapi Service IP is still being provisioned.
Tests¶
api/tests/test_auto_warmup.py- Asserts the reconciler forwards
storage_resource_group=rg-elbon enqueue. - New
test_warmup_database_fails_fast_when_storage_resource_group_missingpins the fail-fast contract; uses a patchedlist_databasesthat must not be called, proving the validation runs before any Storage access. - The pre-existing
test_warmup_database_auto_strict_waits_for_requested_ready_nodesnow passesstorage_resource_group="rg-elb"to satisfy the new guard before it reaches the deferred-path assertions. api/tests/test_warmup_database_readiness.py- The three readiness tests
(
copying/partial/update_in_progress) now passstorage_resource_group="rg-elb"so the new fail-fast does not short-circuit them before the readiness checks run.
SPA fall-backs left in place (intentional)¶
web/src/components/WarmupSection.tsx,
web/src/components/ClusterItem/ClusterItem.tsx, and
web/src/components/cards/ClusterCard/useClusterProvisioning.ts
still carry storageResourceGroup || resourceGroup fall-backs.
Removing them would regress single-RG deployments (the most common
demo configuration) without adding safety: the backend's new
roles_failed → RuntimeError path makes the silent skip
impossible. A misconfigured fallback now produces an explicit ARM
error with the wrong RG name in it, which is the desired UX —
the dashboard tells the operator exactly which RG was tried.
Validation evidence¶
uv run pytest -q api/tests→ 1551 passed, 0 failed (the pre-existing flakytest_run_truncates_stdout_above_capenv case is unrelated and intermittent).uv run pytest -q api/tests/test_auto_warmup.py api/tests/test_warmup_route.py api/tests/test_azure_tasks.py api/tests/test_azure_provision_aks.py api/tests/test_openapi_deploy_contract.py api/tests/test_rbac_preflight.py→ 73 passed.uv run ruff checkover the seven touched modules → All checks passed.git diff --statconfirms only the nine intended files (seven api / two tests) were modified.