Public HTTPS hardening + Auto OpenAPI deploy (2026-05-28)¶
Motivation¶
On elb-cluster-01 the Public HTTPS panel showed NOT EXPOSED for hours
even though the operator clicked Enable. Live triage surfaced three
independent root causes that the dashboard either could not surface or
silently absorbed:
_FALLBACK_OPERATOR_EMAIL = "noreply@elb-dashboard.local"failed Let's Encrypt ACME registration withurn:ietf:params:acme:error:invalidContact("Domain name does not end with a valid public suffix (TLD)") so the whole pipeline silently failed at Step 6.- The
ingress-nginx-controllerPod sat inPendingfor 15h with0/3 nodes are available: 1 Insufficient cpu, 2 node(s) had untolerated taint(s)because thesystempoolpatch landed it on a single DS2_v2 node that AKS system add-ons had already requested to 99 %. - The
elb-openapinamespace did not exist at all — the OpenAPI Service is not deployed automatically when AKS starts, so a dashboard-driven AKS stop/start cycle leaves the API surface dark until an operator clicks Deploy again.
User-facing change¶
- Settings → Public HTTPS now auto-fills
Operator emailfrom the signed-in caller (/api/meupn, with MSALaccount.usernameas a fallback). The Enable button is disabled when the email is empty or uses a private-use TLD that Let's Encrypt will reject (.local,.localhost,.internal,.test,.example, …). The hint line explains the rejection inline so operators can fix it without reading server logs. - AKS Start (lifecycle) and AKS Provision both now enqueue
deploy_openapi_serviceautomatically. Existing explicitauto_openapipayloads still win; absent ones fall back toPLATFORM_ACR_NAME/STORAGE_ACCOUNT_NAME/AZURE_RESOURCE_GROUP/AZURE_TENANT_IDenv defaults that are already injected on the api / worker sidecars. Opt-out: setELB_AUTO_OPENAPI_DEPLOY=falseon the Container App revision. - ingress-nginx + cert-manager now schedule on the blastpool
(user-mode) instead of the systempool. The toleration switches
from
CriticalAddonsOnly:Existstoworkload=blast:NoScheduleand thenodeSelectorswitches frommode=systemtomode=userso the starved systempool is no longer in the path.
API / IaC diff summary¶
POST /api/aks/openapi/public-https— adds_validate_operator_emaildefence-in-depth: 400 when empty / malformed / private-TLD. Mirrors the SPA gate so a stale browser tab cannot re-introduce the regression.api/tasks/openapi/public_https.py::_resolve_operator_email— drops thenoreply@elb-dashboard.localfallback. The task now raisesValueError(caught at the top ofsetup_openapi_public_httpsand returned as{status:"failed", step:"resolve_operator_email"}) when neither caller norELB_OPERATOR_EMAILenv supplies a value.api/services/k8s/ingress.py— renames the patch helper topatch_manifest_for_workload_pool(andfetch_install_manifest_for_workload_pool) withsystem_pool-named backward-compat aliases. New constantsWORKLOAD_POOL_TOLERATION = {workload, Equal, blast, NoSchedule}andWORKLOAD_POOL_NODE_SELECTOR = {kubernetes.azure.com/mode: user}.SYSTEM_POOL_*aliases preserved so callers / tests do not break.api/tasks/openapi/auto_deploy.py(new) — single source of truth for the auto-deploy policy (AUTO_DEPLOY_ENV,auto_deploy_enabled,build_auto_openapi_payload,enqueue_openapi_deploy_after_aks_event).api/tasks/azure/lifecycle.py::start_aks— now always tries to enqueuedeploy_openapi_service(via the helper) unless the opt-out env is set; explicitauto_openapipayload still wins.api/tasks/azure/provision.py::provision_aks— same helper invoked immediately after_publish(..., "completed", ...). Returns a newopenapi_task_idfield in the success payload.web/src/components/SettingsPanel.tsx::PublicHttpsSection— addsisPublicLetsEncryptEmailvalidator + auto-fill effect that callsmeApi.get()and falls back to MSALaccount.username. The Enable button is gated oncanEnable = canAct && emailValid. TheOperator emailfield label drops the(optional)suffix.
No Bicep / IaC changes.
Validation evidence¶
uv run pytest -q api/tests→ 1562 passed in 30.37s.- Focused suites:
api/tests/test_openapi_public_https.py— 30 tests including the newtest_route_validate_operator_email_blocks_private_tldsand the updatedtest_operator_email_resolution(now assertsValueErroron empty input).api/tests/test_azure_tasks.py— addstest_start_aks_auto_deploys_when_no_explicit_payloadandtest_start_aks_skips_openapi_when_opt_out_env_set; updatestest_start_aks_enqueues_openapi_after_cluster_startfor the newtenant_id/caller_oidkwargs.api/tests/test_warmup_route.pyandapi/tests/test_openapi_deploy_contract.pyunaffected and still green.uv run ruff check api→ All checks passed.cd web && npm run -s build→ built in 4.41s (no errors).cd web && npx vitest run→ 52 files / 389 tests passed.
Files touched¶
api/routes/aks/openapi.py
api/services/k8s/ingress.py
api/tasks/azure/lifecycle.py
api/tasks/azure/provision.py
api/tasks/openapi/__init__.py
api/tasks/openapi/auto_deploy.py (new)
api/tasks/openapi/public_https.py
api/tests/test_azure_tasks.py
api/tests/test_openapi_public_https.py
web/src/components/SettingsPanel.tsx