Files
computer-playbook/plugins/lookup/addon_env_flags.py
T
kevinveenbirkenbachandClaude Opus 5 cfa2e19a2f fix(addons): resolve a bridge through the service registry, not only the role name
_any_bridge_partner_deployed matched a bridge against deployed role ids by equality or '-<bridge>' suffix. That reaches a partner whose entity carries the name (matrix -> web-app-matrix) but never one registered under a service key: 'sso' is provided by web-app-keycloak and 'ldap' by svc-db-openldap, neither of which ends in the bridge name. Those five declarations therefore evaluated to false in every run, so their addon flags were false everywhere and skipUnlessAddonEnabled skipped the specs in every variant - silently, because a gate that never opens looks exactly like a variant that does not apply.

The lookup now resolves a bridge through build_service_registry_from_applications first and keeps the entity-suffix rule as the fallback, so both spellings work. Existing bridge tests are unaffected: their applications stub yields an empty registry and takes the old path.

The new lint asserts that every bridges: value names a service some meta/services.yml declares. It fails on a typo or an invented name while staying agnostic about which run deploys the service, which is the lookup's question at runtime. Verified both ways: no findings against the real service set, thirty against an empty one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 14:04:14 +02:00

141 lines
5.3 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
from utils.roles.applications.services.registry import (
build_service_registry_from_applications,
)
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, service_registry):
"""A bridged addon's partner counts as deployed when a deployed role either
provides the bridged service (``sso`` -> ``web-app-keycloak``, ``ldap`` ->
``svc-db-openldap``) or carries the bridge name as its entity
(``matrix`` -> ``web-app-matrix``)."""
for bridge in bridges:
name = str(bridge).strip()
if not name:
continue
entry = service_registry.get(name) if service_registry else None
provider = entry.get("role") if isinstance(entry, dict) else None
if provider and provider in deployed_roles:
return True
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)
service_registry = build_service_registry_from_applications(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, service_registry
)
lines.append(f"{env_key(addon_id)}={'true' if active else 'false'}")
return ["\n".join(lines)]