Per-cluster cluster context for the external-BLAST outbound resolver¶
Motivation¶
Issue #26 tracks the outbound elb-openapi resolver reading the base URL and API token from process-global, single-key caches. In a single Container App revision that manages more than one AKS cluster (each with its own custom domain + token), those globals hold the most-recently-touched cluster's values, so an outbound call made while the user is on cluster A can be sent to cluster B's domain with B's token — confusing 401s or cross-cluster exposure once two clusters each have a custom domain.
The storage side (save_openapi_* / get_openapi_*), get_public_tls_base_url,
and the API Reference proxy (commit a7d9949) are already per-cluster keyed.
This change closes the next slice: the external_blast client's own resolvers.
User-facing change¶
No behaviour change for existing callers — every new parameter defaults to empty and the global-key resolution is preserved (charter §12a Rule 4: additive / default-OFF). A caller that supplies the cluster context now resolves that cluster's endpoint and token from the per-cluster cache.
external_blast._base_url(value=None, *, subscription_id="", resource_group="", cluster_name="")— when no explicitvalue/ env override and the full cluster context is supplied, prefersget_public_tls_base_url(<cluster>)(the per-cluster public HTTPS key); a miss falls through to the legacy global runtime key.external_blast._headers(*, api_token=None, internal_token=None, subscription_id="", resource_group="", cluster_name="")— passes the cluster context toget_openapi_api_token(<cluster>)so the per-cluster token key is read first, with the legacy global key as a fallback. The token value is never logged.submit_job/get_job/list_jobsaccept the same three context kwargs (defaulted) and thread them into_base_url/_headers, so an outbound call scoped to cluster A resolves A's base URL and A's token end-to-end.
API / IaC diff summary¶
- api/services/external_blast.py —
_base_url/_headersgain cluster-context kwargs;submit_job/get_job/list_jobsthread them through. - No IaC change. No new dependency.
Scope notes (remaining #26 surface — deferred)¶
- The genuinely context-less facade routes
(api/routes/elastic_blast.py,
the
get_jobfallbacks in api/routes/blast/jobs.py / api/routes/blast/results.py) only carry the sibling'sjob_id, not a cluster, so they still resolve the global key. Making them cluster-correct needs a job→cluster resolver plus a multi-cluster submit request contract — the larger #26 design item that cannot be safely validated without a live multi-cluster deployment. ready(the submit pre-flight) and the streaming download/upload proxy are left on the global path for now; their cluster-aware token-resync path is a follow-up tied to the same resolver work.- httpx connection-pool reuse for the OpenAPI plane (issue #30 candidate fix #4) remains folded into this resolver work, since a single shared client cannot span per-cluster base URLs.
Validation evidence¶
uv run pytest -q api/tests/test_external_blast_cluster_resolver.py— 8 passed. Proves per-cluster base URL + token resolution for clusters A/B, the global fallback for a context-less call and an unknown cluster, the explicit-value short-circuit, and thatsubmit_job/get_job/list_jobsthread the context to the resolvers.uv run pytest -q api/tests/test_external_blast_api.py api/tests/test_openapi_proxy_route.py api/tests/test_openapi_runtime_token_cache.py api/tests/test_openapi_public_https.py api/tests/test_openapi_tls_hook.py— 160 passed.uv run ruff check api/services/external_blast.py api/tests/test_external_blast_cluster_resolver.py— clean.