mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
_fingerprint_mapping memoised digests under id(obj) while holding no reference to obj, so CPython reused the address of a freed mapping and served its digest for an unrelated live one. Measured 398/400 wrong digests for short-lived mappings; in-place mutation was invisible and the domain-index collision guard could be bypassed entirely. Two unit tests pinned that behaviour as the contract — test_id_cache_short_circuits_recompute asserted that mutating a dict in place leaves its fingerprint unchanged — so the defect shipped green. Both now assert the opposite. build_domain_index rebuilt the whole reverse index on every lookup, scanning every application's domains and aliases to answer one key. It is now memoised on that fingerprint, which is only sound because of the fix above. 4x on a 100-app payload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
4.0 KiB
Python
151 lines
4.0 KiB
Python
#
|
|
# Shared helpers for indexing application domains from
|
|
# applications[app_id].domains.{canonical,aliases}.
|
|
#
|
|
# Supports canonical/aliases being:
|
|
# - str
|
|
# - list
|
|
# - dict (possibly nested, like "web/api/view" variants)
|
|
#
|
|
# Note:
|
|
# This module does NOT template Jinja. It expects values to already
|
|
# be rendered at lookup runtime.
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable
|
|
|
|
_DOMAIN_INDEX_CACHE: dict[tuple, dict[str, str]] = {}
|
|
|
|
|
|
def _norm_domain(value: Any) -> str:
|
|
s = str(value).strip() if value is not None else ""
|
|
return s.lower()
|
|
|
|
|
|
def _flatten_str_values(value: Any) -> list[str]:
|
|
"""
|
|
Recursively flatten strings from value.
|
|
Accepts str | list | dict | nested combinations.
|
|
Returns list[str] preserving discovery order (best effort).
|
|
"""
|
|
out: list[str] = []
|
|
|
|
def walk(v: Any) -> None:
|
|
if v is None:
|
|
return
|
|
if isinstance(v, str):
|
|
s = v.strip()
|
|
if s:
|
|
out.append(s)
|
|
return
|
|
if isinstance(v, list):
|
|
for item in v:
|
|
walk(item)
|
|
return
|
|
if isinstance(v, dict):
|
|
for vv in v.values():
|
|
walk(vv)
|
|
return
|
|
|
|
walk(value)
|
|
return out
|
|
|
|
|
|
def iter_app_domains(app_conf: Any, include_aliases: bool = True) -> Iterable[str]:
|
|
"""
|
|
Yield all canonical + alias domains from an app config.
|
|
|
|
Expected structure:
|
|
applications[app_id].domains.canonical
|
|
applications[app_id].domains.aliases
|
|
"""
|
|
if not isinstance(app_conf, dict):
|
|
return []
|
|
|
|
domains = app_conf.get("domains", {})
|
|
if not isinstance(domains, dict):
|
|
return []
|
|
|
|
canonical = domains.get("canonical", [])
|
|
result: list[str] = []
|
|
result.extend(_flatten_str_values(canonical))
|
|
if include_aliases:
|
|
aliases = domains.get("aliases", [])
|
|
result.extend(_flatten_str_values(aliases))
|
|
return result
|
|
|
|
|
|
def build_domain_index(
|
|
applications: dict[str, Any], include_aliases: bool = True
|
|
) -> dict[str, str]:
|
|
"""
|
|
Build a case-insensitive domain -> application_id index.
|
|
If the same domain appears in multiple apps (case-insensitive), raises an error.
|
|
|
|
Cached keyed on (applications fingerprint, include_aliases); the result is
|
|
returned as-is, so callers MUST treat it as read-only.
|
|
"""
|
|
if not isinstance(applications, dict):
|
|
raise AnsibleError("application_domain_index: applications must be a dict")
|
|
|
|
from utils.cache.base import _fingerprint_mapping
|
|
|
|
cache_key = (_fingerprint_mapping(applications), bool(include_aliases))
|
|
cached = _DOMAIN_INDEX_CACHE.get(cache_key)
|
|
if cached is not None:
|
|
return cached
|
|
|
|
index: dict[str, str] = {}
|
|
collisions: dict[str, set[str]] = {}
|
|
|
|
for app_id, app_conf in applications.items():
|
|
for d in iter_app_domains(app_conf, include_aliases=include_aliases):
|
|
nd = _norm_domain(d)
|
|
if not nd:
|
|
continue
|
|
|
|
if nd in index and index[nd] != app_id:
|
|
collisions.setdefault(nd, set()).update({index[nd], app_id})
|
|
else:
|
|
index[nd] = app_id
|
|
|
|
if collisions:
|
|
parts = []
|
|
for domain, apps in sorted(collisions.items(), key=lambda x: x[0]):
|
|
parts.append(f"{domain}: {sorted(apps)}")
|
|
raise AnsibleError(
|
|
"application_domain_index: domain collision across applications (ambiguous mapping): "
|
|
+ "; ".join(parts)
|
|
)
|
|
|
|
_DOMAIN_INDEX_CACHE[cache_key] = index
|
|
return index
|
|
|
|
|
|
def _reset() -> None:
|
|
_DOMAIN_INDEX_CACHE.clear()
|
|
|
|
|
|
def resolve_app_id_for_domain(
|
|
applications: dict[str, Any],
|
|
domain: str,
|
|
) -> str | None:
|
|
"""
|
|
Resolve application_id for a given domain (canonical or alias) by scanning
|
|
applications[*].domains.
|
|
|
|
Returns None if not found.
|
|
"""
|
|
nd = _norm_domain(domain)
|
|
if not nd:
|
|
return None
|
|
|
|
idx = build_domain_index(applications)
|
|
return idx.get(nd)
|