mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 03:15:56 +00:00
The dashboard card of every app opened its primary domain, so Matrix led to the Synapse status page and Stalwart to its admin UI. A role can now declare services.dashboard.card: <key of meta/domains.yml canonical>; docker_cards builds the card URL with lookup('canonical_url', app, key), the Apps menu reuses it, and roles_with_service('dashboard') hands the same domain to the tile e2e spec. Matrix sets card: element, Stalwart card: webmail. Without card nothing changes, and the prometheus/matomo target lists are untouched. A lint requires the key to resolve in the role's own domains.canonical.
The Matrix ansible flavor also sets Synapse's web_client_location to Element through matrix_synapse_configuration_extension_yaml, as the compose flavor already does, so matrix.<domain>/ redirects there. Merge note: feature/iam-profile-rbac defines the same extension variable inside the OIDC block; the next merge must combine both into one block (agreed with that session).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
365 lines
12 KiB
Python
365 lines
12 KiB
Python
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from pathlib import Path
|
|
|
|
from ansible.errors import AnsibleError
|
|
from jinja2 import Environment, StrictUndefined, select_autoescape
|
|
|
|
sys.path.insert(
|
|
0,
|
|
str(
|
|
Path(str(Path(__file__).parent))
|
|
/ "../../../../../roles/web-app-dashboard/lookup_plugins"
|
|
),
|
|
)
|
|
|
|
import docker_cards as docker_cards_module
|
|
from docker_cards import LookupModule
|
|
|
|
from utils.roles.mapping import (
|
|
ROLE_FILE_META_INFO,
|
|
ROLE_FILE_META_MAIN,
|
|
ROLE_FILE_VARS_MAIN,
|
|
)
|
|
|
|
|
|
def _ansible_bool(value):
|
|
"""
|
|
Minimal Ansible-like bool filter for unit tests.
|
|
Mirrors common Ansible truthy/falsey handling for strings.
|
|
"""
|
|
if isinstance(value, bool):
|
|
return value
|
|
if value is None:
|
|
return False
|
|
if isinstance(value, (int, float)):
|
|
return value != 0
|
|
if isinstance(value, str):
|
|
v = value.strip().lower()
|
|
if v in {"y", "yes", "true", "on", "1"}:
|
|
return True
|
|
if v in {"n", "no", "false", "off", "0", ""}:
|
|
return False
|
|
return bool(value)
|
|
|
|
|
|
class DummyTemplar:
|
|
"""
|
|
Small, deterministic templating stub for unit tests.
|
|
It is intentionally minimal: only supports rendering Jinja strings
|
|
and provides an Ansible-like `bool` filter.
|
|
"""
|
|
|
|
def __init__(self, variables):
|
|
self._vars = variables
|
|
self._env = Environment(
|
|
undefined=StrictUndefined, autoescape=select_autoescape()
|
|
)
|
|
self._env.filters["bool"] = _ansible_bool
|
|
|
|
def template(self, value):
|
|
if value is None:
|
|
return value
|
|
|
|
if not isinstance(value, str):
|
|
return value
|
|
|
|
if "{{" in value and "}}" in value:
|
|
tmpl = self._env.from_string(value)
|
|
return tmpl.render(**self._vars)
|
|
|
|
return value
|
|
|
|
|
|
class DummyTlsResolveLookup:
|
|
"""
|
|
Deterministic tls stub.
|
|
Emulates: lookup('tls', app_id, 'url.base') -> 'http(s)://domain/'
|
|
"""
|
|
|
|
def __init__(self, templar):
|
|
self._templar = templar
|
|
|
|
def run(self, terms, variables=None, **kwargs):
|
|
variables = variables or {}
|
|
|
|
if not terms or len(terms) not in (1, 2):
|
|
raise AnsibleError(
|
|
"dummy tls: terms must be [app_id] or [app_id, want_path]"
|
|
)
|
|
|
|
app_id = str(terms[0]).strip()
|
|
if not app_id:
|
|
raise AnsibleError("dummy tls: empty term")
|
|
|
|
want = ""
|
|
if len(terms) == 2:
|
|
want = str(terms[1]).strip()
|
|
|
|
domains = variables.get("domains", {})
|
|
if app_id not in domains:
|
|
raise AnsibleError(f"dummy tls: app_id '{app_id}' not in domains")
|
|
|
|
domain_val = domains[app_id]
|
|
if isinstance(domain_val, list):
|
|
domain = domain_val[0] if domain_val else ""
|
|
elif isinstance(domain_val, dict):
|
|
domain = next(iter(domain_val.values()), "")
|
|
else:
|
|
domain = domain_val
|
|
|
|
domain = self._templar.template(domain).strip() if domain else ""
|
|
if not domain:
|
|
raise AnsibleError(f"dummy tls: empty domain for '{app_id}'")
|
|
|
|
tls_enabled = _ansible_bool(variables.get("TLS_ENABLED", True))
|
|
scheme = "https" if tls_enabled else "http"
|
|
base_url = f"{scheme}://{domain}/"
|
|
|
|
if want and want != "url.base":
|
|
raise AnsibleError(f"dummy tls: unsupported want_path='{want}'")
|
|
|
|
return [base_url]
|
|
|
|
|
|
class DummyCanonicalUrlLookup:
|
|
"""
|
|
Deterministic canonical_url stub.
|
|
Emulates: lookup('canonical_url', app_id, key) -> 'http(s)://<domains[app_id][key]>'
|
|
"""
|
|
|
|
def run(self, terms, variables=None, **kwargs):
|
|
variables = variables or {}
|
|
app_id, key = terms
|
|
app_domains = variables.get("domains", {}).get(app_id)
|
|
if not isinstance(app_domains, dict) or key not in app_domains:
|
|
raise AnsibleError(
|
|
f"dummy canonical_url: '{app_id}' has no canonical '{key}'"
|
|
)
|
|
scheme = (
|
|
"https" if _ansible_bool(variables.get("TLS_ENABLED", True)) else "http"
|
|
)
|
|
return [f"{scheme}://{app_domains[key]}"]
|
|
|
|
|
|
class TestDockerCardsLookup(unittest.TestCase):
|
|
def setUp(self):
|
|
self.test_roles_dir = tempfile.mkdtemp(prefix="test_roles_")
|
|
|
|
self.role_name = "web-app-dashboard"
|
|
self.role_dir = str(Path(self.test_roles_dir) / self.role_name)
|
|
Path(str(Path(self.role_dir) / "meta")).mkdir(parents=True)
|
|
Path(str(Path(self.role_dir) / "vars")).mkdir(parents=True)
|
|
|
|
vars_main = str(Path(self.role_dir) / ROLE_FILE_VARS_MAIN)
|
|
with Path(vars_main).open("w", encoding="utf-8") as f:
|
|
f.write("application_id: portfolio\n")
|
|
|
|
readme_path = str(Path(self.role_dir) / "README.md")
|
|
with Path(readme_path).open("w", encoding="utf-8") as f:
|
|
f.write("# Portfolio Application\nThis is a sample portfolio role.")
|
|
|
|
meta_main_path = str(Path(self.role_dir) / ROLE_FILE_META_MAIN)
|
|
meta_yaml = """
|
|
galaxy_info:
|
|
description: "A role for deploying a portfolio application."
|
|
"""
|
|
with Path(meta_main_path).open("w", encoding="utf-8") as f:
|
|
f.write(meta_yaml)
|
|
|
|
meta_info_path = str(Path(self.role_dir) / ROLE_FILE_META_INFO)
|
|
info_yaml = """
|
|
logo:
|
|
class: fa-solid fa-briefcase
|
|
"""
|
|
with Path(meta_info_path).open("w", encoding="utf-8") as f:
|
|
f.write(info_yaml)
|
|
|
|
self._orig_lookup_get = docker_cards_module.lookup_loader.get
|
|
|
|
class _VarsLookup:
|
|
def __init__(self, key):
|
|
self._key = key
|
|
|
|
def run(self, terms, variables=None, **kwargs):
|
|
return [(variables or {}).get(self._key, {})]
|
|
|
|
def _patched_get(name, loader=None, templar=None):
|
|
if name == "applications":
|
|
return _VarsLookup("applications")
|
|
if name == "domains":
|
|
return _VarsLookup("domains")
|
|
if name == "tls":
|
|
return DummyTlsResolveLookup(templar)
|
|
if name == "canonical_url":
|
|
return DummyCanonicalUrlLookup()
|
|
raise AnsibleError(f"Unexpected lookup requested: {name}")
|
|
|
|
docker_cards_module.lookup_loader.get = _patched_get
|
|
|
|
def tearDown(self):
|
|
docker_cards_module.lookup_loader.get = self._orig_lookup_get
|
|
|
|
shutil.rmtree(self.test_roles_dir)
|
|
|
|
def _base_fake_variables(self):
|
|
return {
|
|
"domains": {"portfolio": "myportfolio.com"},
|
|
"applications": {
|
|
"portfolio": {"services": {"dashboard": {"enabled": True}}}
|
|
},
|
|
"group_names": ["portfolio"],
|
|
"TLS_ENABLED": True,
|
|
"TLS_MODE": "letsencrypt",
|
|
"LETSENCRYPT_BASE_PATH": "/etc/letsencrypt",
|
|
"LETSENCRYPT_LIVE_PATH": "/etc/letsencrypt/live",
|
|
}
|
|
|
|
def _run_lookup(self, lookup_module, fake_variables):
|
|
lookup_module._templar = DummyTemplar(fake_variables)
|
|
lookup_module._loader = mock.MagicMock()
|
|
return lookup_module.run([self.test_roles_dir], variables=fake_variables)
|
|
|
|
def test_lookup_when_group_includes_application_id(self):
|
|
lookup_module = LookupModule()
|
|
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["TLS_ENABLED"] = True
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertIsInstance(result, list)
|
|
self.assertEqual(len(result), 1)
|
|
|
|
cards = result[0]
|
|
self.assertIsInstance(cards, list)
|
|
self.assertEqual(len(cards), 1)
|
|
|
|
card = cards[0]
|
|
self.assertEqual(card["title"], "Portfolio Application")
|
|
self.assertEqual(card["text"], "A role for deploying a portfolio application.")
|
|
self.assertEqual(card["icon"]["class"], "fa-solid fa-briefcase")
|
|
self.assertEqual(card["url"], "https://myportfolio.com")
|
|
self.assertTrue(card["iframe"])
|
|
|
|
def test_iframe_flag_defaults_to_enabled(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertTrue(result[0][0]["iframe"])
|
|
|
|
def test_iframe_flag_false_decouples_from_enabled(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["applications"]["portfolio"]["services"]["dashboard"][
|
|
"iframe"
|
|
] = False
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
cards = result[0]
|
|
self.assertEqual(len(cards), 1)
|
|
self.assertFalse(cards[0]["iframe"])
|
|
|
|
def test_lookup_url_uses_https_when_tls_enabled_true(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["TLS_ENABLED"] = True
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
|
|
|
|
def test_lookup_url_uses_http_when_tls_enabled_false(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["TLS_ENABLED"] = False
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
|
|
|
|
def test_lookup_when_group_excludes_application_id(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["group_names"] = []
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(len(result[0]), 0)
|
|
|
|
def test_lookup_url_renders_domain_url_jinja(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
|
|
fake_variables["domains"] = {"portfolio": "{{ DOMAIN_PRIMARY }}"}
|
|
fake_variables["DOMAIN_PRIMARY"] = "myportfolio.com"
|
|
fake_variables["TLS_ENABLED"] = True
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
|
|
|
|
def test_lookup_url_https_when_tls_enabled_is_string(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
|
|
fake_variables["TLS_ENABLED"] = "true"
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
|
|
|
|
def test_lookup_url_http_when_tls_enabled_is_string(self):
|
|
lookup_module = LookupModule()
|
|
fake_variables = self._base_fake_variables()
|
|
|
|
fake_variables["TLS_ENABLED"] = "false"
|
|
|
|
result = self._run_lookup(lookup_module, fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "http://myportfolio.com")
|
|
|
|
def _card_target_variables(self, dashboard):
|
|
fake_variables = self._base_fake_variables()
|
|
fake_variables["domains"] = {
|
|
"portfolio": {
|
|
"www": "myportfolio.com",
|
|
"client": "client.myportfolio.com",
|
|
}
|
|
}
|
|
fake_variables["applications"]["portfolio"]["services"]["dashboard"] = dashboard
|
|
return fake_variables
|
|
|
|
def test_card_url_targets_the_card_key(self):
|
|
fake_variables = self._card_target_variables(
|
|
{"enabled": True, "card": "client"}
|
|
)
|
|
|
|
result = self._run_lookup(LookupModule(), fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "https://client.myportfolio.com")
|
|
|
|
def test_card_url_without_card_key_targets_the_primary_domain(self):
|
|
fake_variables = self._card_target_variables({"enabled": True})
|
|
|
|
result = self._run_lookup(LookupModule(), fake_variables)
|
|
|
|
self.assertEqual(result[0][0]["url"], "https://myportfolio.com")
|
|
|
|
def test_unknown_card_key_fails_loudly(self):
|
|
fake_variables = self._card_target_variables({"enabled": True, "card": "nope"})
|
|
|
|
with self.assertRaisesRegex(AnsibleError, "nope"):
|
|
self._run_lookup(LookupModule(), fake_variables)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|