mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The front proxy hardcoded [200, 301, 302] and escalated to a full OpenResty redeploy on anything else. Apps that answer 403, 404 or 405 by design tripped it on every run: 19 escalations in one Nextcloud job at a measured median of 40.5s each. roles/<app>/meta/server.yml already declares status_codes per canonical key, and sys-ctl-hlth-webserver already reads it. Move the resolution into utils/roles/applications/status_codes.py so both consumers share one definition of DEFAULT_OK, and expose it to templates as the accepted_status_codes filter. The proxy gate takes the union of the default and the declaration rather than replacing it. Most declarations are narrower than the triple, so replacing would escalate vhosts that answer perfectly well. Contract checking stays with sys-ctl-hlth-webserver, which keeps replacement semantics. _normalize_codes and _valid_http_code became dead in the health filter and are removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
3.8 KiB
Python
120 lines
3.8 KiB
Python
"""HTTP status codes for a deployed domain.
|
|
|
|
``roles/<app>/meta/server.yml`` declares ``status_codes`` keyed like
|
|
``domains.canonical``; it is the single source of truth for every prober.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
from utils.roles.applications.config import get
|
|
|
|
DEFAULT_OK = [200, 302, 301]
|
|
|
|
|
|
def normalize_codes(value) -> list[int]:
|
|
"""Return *value* as a list of plausible HTTP status codes.
|
|
|
|
Args:
|
|
value: a scalar, list or mapping straight out of the declaration.
|
|
|
|
Returns:
|
|
The codes as de-duplicated integers, dropping anything outside 100-599.
|
|
"""
|
|
if value is None:
|
|
return []
|
|
candidates = value if isinstance(value, (list, tuple, set)) else [value]
|
|
codes: list[int] = []
|
|
for candidate in candidates:
|
|
if isinstance(candidate, bool):
|
|
continue
|
|
try:
|
|
code = int(candidate)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if 100 <= code <= 599 and code not in codes:
|
|
codes.append(code)
|
|
return codes
|
|
|
|
|
|
def codes_by_key(raw) -> dict[str, list[int]]:
|
|
"""Normalize a raw ``server.status_codes`` declaration.
|
|
|
|
Args:
|
|
raw: the declaration as read from the app config.
|
|
|
|
Returns:
|
|
The declared keys mapped to their codes, dropping keys that declare
|
|
nothing usable.
|
|
"""
|
|
if not isinstance(raw, Mapping):
|
|
return {}
|
|
resolved: dict[str, list[int]] = {}
|
|
for key, value in raw.items():
|
|
codes = normalize_codes(value)
|
|
if codes:
|
|
resolved[str(key)] = codes
|
|
return resolved
|
|
|
|
|
|
def declared_status_codes(applications, app_id: str, domain: str) -> list[int]:
|
|
"""Return what *app_id* declares for *domain*, or an empty list.
|
|
|
|
The vhost is identified by the ``domains.canonical`` key that carries the
|
|
domain, which is the same key ``server.status_codes`` uses. A domain that
|
|
is not canonical, or a key that declares nothing, falls back to the
|
|
declared ``default``.
|
|
|
|
Args:
|
|
applications: the applications SPOT.
|
|
app_id: the role the domain belongs to.
|
|
domain: the fully qualified domain being probed.
|
|
|
|
Returns:
|
|
The declared codes, empty when the app declares none for this domain.
|
|
"""
|
|
declared = codes_by_key(
|
|
get(applications, app_id, "server.status_codes", strict=False, default={})
|
|
)
|
|
if not declared:
|
|
return []
|
|
|
|
wanted = str(domain or "").strip()
|
|
canonical = get(applications, app_id, "domains.canonical", strict=False, default=[])
|
|
if wanted and isinstance(canonical, Mapping):
|
|
for key, domains in canonical.items():
|
|
entries = domains if isinstance(domains, (list, tuple, set)) else [domains]
|
|
if wanted in {str(entry).strip() for entry in entries if entry}:
|
|
codes = declared.get(str(key))
|
|
if codes:
|
|
return list(codes)
|
|
break
|
|
|
|
return list(declared.get("default") or [])
|
|
|
|
|
|
def accepted_status_codes(applications, app_id: str, domain: str) -> list[int]:
|
|
"""Return the codes that prove *domain* is being served at all.
|
|
|
|
Most declarations are narrower than :data:`DEFAULT_OK`, so this gate adds
|
|
to the default instead of replacing it; narrowing it would escalate vhosts
|
|
that answer perfectly well. Contract checking is a different question and
|
|
belongs to ``sys-ctl-hlth-webserver``.
|
|
|
|
Args:
|
|
applications: the applications SPOT.
|
|
app_id: the role the domain belongs to.
|
|
domain: the fully qualified domain being probed.
|
|
|
|
Returns:
|
|
:data:`DEFAULT_OK` plus whatever the app declares for this domain.
|
|
"""
|
|
accepted = list(DEFAULT_OK)
|
|
accepted.extend(
|
|
code
|
|
for code in declared_status_codes(applications, app_id, domain)
|
|
if code not in accepted
|
|
)
|
|
return accepted
|