mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
required is the install-failure switch, not an enablement switch:
docs/requirements/026-unified-addon-syntax.md:33 and :111 define it that way,
it is implemented only as failed_when guards in web-app-wordpress and
web-app-mattermost, and no variant and no inventory pins it anywhere. ANDing
it into the gate therefore held the flag of every 'required: false' addon at
false in every variant and every mode since 70786aa126 created the suite, so
those Playwright specs reported as skipped with the plausible reason 'addon
disabled or not deployed' while asserting nothing.
Measured against this commit's parent: 32 addon specs declare
'required: false', 24 of them in the 11 roles that call the lookup, out of the
89 addon specs those roles ship. Those 24 move from always-skipped to
executing. The flag reaches no deploy configuration, so the worst case is a red
Playwright job rather than an open surface - the old behaviour was the silent
one.
Both test files stated the old contract and are corrected with it.
Residual: the repairs for several of the specs this wakes - discourse's akismet
assertions and its missing sso gates, moodle's two addons that no install path
covers - are not in this commit. A round over those roles is red until they
land.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
142 lines
5.4 KiB
Python
142 lines
5.4 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 the round's deployed role closure 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("group_names")
|
|
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()} or None
|
|
|
|
|
|
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`` when the addon is ``enabled`` and, where it declares
|
|
``bridges``, at least one bridged partner is deployed in this round.
|
|
``required`` does not enter the gate: it says whether a failed install
|
|
aborts the play, not whether the addon runs, and ANDing it here made the
|
|
spec of every optional addon unreachable in every variant, because no
|
|
variant pins ``required`` while variants do pin addon ``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))
|
|
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)]
|