mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Consumer lookup plugins now reach the merged config through the Ansible
lookup loader instead of importing get_merged_applications /
get_merged_domains from utils.cache. The merge stays one cached
implementation surfaced via the applications/config/domains/users lookups;
consumers call lookup_loader.get(...).run() (the compose_networks idiom).
- get_merged_applications walks the whole meta/ tree recursively
(meta/**/*.yml to applications.<app>.<path>, addons and any depth) and
folds the raw meta/volumes.yml subtree onto applications.<app>.volumes
after the templar render so the Jinja source: strings stay unrendered.
- 31 consumer lookups rerouted; providers (applications/config/
applications_current_play/users/domains) and the raw-volume accessors
(volume/nginx) keep the cache import behind # nocheck: lookup-cache-import.
- New lint test_no_cache_imports_in_lookups enforces the contract.
- nfs_flavor is now the flavor SPOT (svc-storage-nfs-server + nfs_client_src
resolve via lookup('nfs_flavor')); removed the unused STORAGE_NFS_FLAVOR
group_var. Extracted extract_default_credentials to
utils/cache/schema_credentials.py to keep applications.py under the cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.loader import lookup_loader
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
from utils.cache.base import _render_with_templar
|
|
from utils.roles.applications.config import get
|
|
|
|
ENV_SUFFIX = "_ADDON_ENABLED"
|
|
_TRUE_TOKENS = {"true", "1", "yes", "on", "t", "y"}
|
|
|
|
|
|
def env_key(addon_id: str) -> str:
|
|
"""Mirror addon-gating.js envKey(): upper-case, non-alphanumeric runs -> '_'."""
|
|
return re.sub(r"[^A-Z0-9]+", "_", str(addon_id).upper()) + ENV_SUFFIX
|
|
|
|
|
|
def _is_enabled(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
return str(value).strip().lower() in _TRUE_TOKENS
|
|
|
|
|
|
def _resolve_deployed_roles(variables, templar, applications):
|
|
"""Resolve TEST_E2E_PLAYWRIGHT_APPS (the deployed playwright role ids) to a set.
|
|
Returns None when it cannot be resolved, in which case bridge-deployment
|
|
gating is skipped (no behaviour change)."""
|
|
raw = (variables or {}).get("TEST_E2E_PLAYWRIGHT_APPS")
|
|
if raw is None:
|
|
return None
|
|
resolved = raw
|
|
if isinstance(raw, str):
|
|
try:
|
|
resolved = _render_with_templar(
|
|
raw, templar=templar, variables=variables, raw_applications=applications
|
|
)
|
|
except Exception:
|
|
return None
|
|
if isinstance(resolved, str):
|
|
resolved = [r for r in re.split(r"[\s,]+", resolved.strip()) if r]
|
|
if not isinstance(resolved, (list, tuple, set)):
|
|
return None
|
|
return {str(r).strip() for r in resolved if str(r).strip()}
|
|
|
|
|
|
def _any_bridge_partner_deployed(bridges, deployed_roles):
|
|
"""A bridged addon's partner counts as deployed iff some deployed role id
|
|
equals or ends with the bridge name (web-app-<bridge>, web-svc-<bridge>, ...)."""
|
|
for bridge in bridges:
|
|
name = str(bridge).strip()
|
|
if not name:
|
|
continue
|
|
variants = {name, name.replace("_", "-")}
|
|
for role in deployed_roles:
|
|
if role in variants or any(role.endswith("-" + v) for v in variants):
|
|
return True
|
|
return False
|
|
|
|
|
|
class LookupModule(LookupBase):
|
|
"""
|
|
lookup('addon_env_flags', application_id)
|
|
|
|
Single source of truth for the per-addon enable flags consumed by the
|
|
Playwright addon-gating helper (``tests/addon-gating.js`` ->
|
|
``skipUnlessAddonEnabled``). Replaces the error-prone per-template Jinja
|
|
loop: it resolves the materialised ``addons`` mapping for ``application_id``
|
|
and returns one ``<ADDON_ID>_ADDON_ENABLED=<true|false>`` line per declared
|
|
addon (sorted by env key, newline-joined), where the env-key derivation
|
|
matches addon-gating.js exactly.
|
|
|
|
A flag is ``true`` only when the addon is both ``enabled`` AND
|
|
``required``. Optional addons (``required: false``) are intentionally
|
|
skipped by the spec suite — they are a variant axis, not a guaranteed
|
|
surface — so their gate flag is ``false`` even when enabled.
|
|
"""
|
|
|
|
def run(self, terms, variables: dict[str, Any] | None = None, **kwargs):
|
|
if not terms or len(terms) != 1:
|
|
raise AnsibleError(
|
|
"lookup('addon_env_flags', application_id) expects exactly one term."
|
|
)
|
|
|
|
application_id = terms[0]
|
|
templar = getattr(self, "_templar", None)
|
|
variables = variables or getattr(self._templar, "available_variables", {}) or {}
|
|
|
|
applications = lookup_loader.get(
|
|
"applications", loader=self._loader, templar=templar
|
|
).run([], variables=variables)[0]
|
|
addons = get(
|
|
applications=applications,
|
|
application_id=application_id,
|
|
config_path="addons",
|
|
strict=False,
|
|
default={},
|
|
skip_missing_app=True,
|
|
)
|
|
addons = _render_with_templar(
|
|
addons,
|
|
templar=templar,
|
|
variables=variables,
|
|
raw_applications=applications,
|
|
)
|
|
if not isinstance(addons, dict):
|
|
addons = {}
|
|
|
|
deployed_roles = _resolve_deployed_roles(variables, templar, applications)
|
|
|
|
lines = []
|
|
for addon_id in sorted(addons, key=env_key):
|
|
spec = (
|
|
addons.get(addon_id) if isinstance(addons.get(addon_id), dict) else {}
|
|
)
|
|
active = _is_enabled(spec.get("enabled", False)) and _is_enabled(
|
|
spec.get("required", False)
|
|
)
|
|
if active and deployed_roles is not None:
|
|
bridges = spec.get("bridges")
|
|
if isinstance(bridges, list) and bridges:
|
|
active = _any_bridge_partner_deployed(bridges, deployed_roles)
|
|
lines.append(f"{env_key(addon_id)}={'true' if active else 'false'}")
|
|
|
|
return ["\n".join(lines)]
|