mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
328113e7c added a guard that reads every group_vars/all file to confirm each
GROUP_GATED_CONSTANTS entry is really defined, and read them with
path.read_text(). test_no_uncached_file_reads rejects that under tests/: the
cached read_text shares one decoded read per file across the whole session, and
these files are read by several other tests already.
The nocheck escape does not apply. It covers reads outside the project tree,
and group_vars/all is inside it.
What the guard asserts is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
378 lines
13 KiB
Python
378 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.cache.files import PROJECT_ROOT, read_text
|
|
from utils.cache.yaml import dump_yaml_str
|
|
from utils.roles.applications.services.registry import (
|
|
GROUP_GATED_CONSTANTS,
|
|
ServiceRegistryError,
|
|
build_service_registry_from_applications,
|
|
detect_service_bucket,
|
|
detect_service_channel,
|
|
is_explicit_truth,
|
|
ordered_primary_service_entries,
|
|
resolve_service_dependency_roles_from_config,
|
|
)
|
|
|
|
|
|
class TestServiceRegistryDiscovery(unittest.TestCase):
|
|
"""Per, the materialised application payload exposes services
|
|
under the bare ``services`` key (no ``compose.services`` wrapper)."""
|
|
|
|
def test_discovery_reads_provides_canonical_shared_and_enabled(self):
|
|
applications = {
|
|
"web-app-keycloak": {
|
|
"services": {
|
|
"keycloak": {
|
|
"enabled": False,
|
|
"shared": True,
|
|
"provides": "oidc",
|
|
}
|
|
}
|
|
},
|
|
"web-svc-cdn": {
|
|
"services": {
|
|
"cdn": {"enabled": False, "shared": True},
|
|
"css": {"enabled": True, "shared": True, "canonical": "cdn"},
|
|
"javascript": {
|
|
"enabled": False,
|
|
"shared": True,
|
|
"canonical": "cdn",
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
registry = build_service_registry_from_applications(applications)
|
|
|
|
self.assertEqual(registry["oidc"]["role"], "web-app-keycloak")
|
|
self.assertEqual(registry["oidc"]["entity_name"], "keycloak")
|
|
self.assertTrue(registry["oidc"]["shared"])
|
|
self.assertFalse(registry["oidc"]["enabled"])
|
|
|
|
self.assertEqual(registry["cdn"]["role"], "web-svc-cdn")
|
|
self.assertNotIn("canonical", registry["cdn"])
|
|
self.assertEqual(registry["css"]["canonical"], "cdn")
|
|
self.assertTrue(registry["css"]["enabled"])
|
|
self.assertTrue(registry["javascript"]["shared"])
|
|
|
|
def test_entity_name_derivation_for_relevant_role_prefixes(self):
|
|
applications = {
|
|
"web-app-dashboard": {
|
|
"services": {"dashboard": {"enabled": False, "shared": True}}
|
|
},
|
|
"web-svc-file": {"services": {"file": {"enabled": False, "shared": True}}},
|
|
"svc-db-mariadb": {
|
|
"services": {"mariadb": {"enabled": False, "shared": True}}
|
|
},
|
|
"svc-ai-ollama": {
|
|
"services": {"ollama": {"enabled": False, "shared": True}}
|
|
},
|
|
}
|
|
|
|
registry = build_service_registry_from_applications(applications)
|
|
|
|
self.assertIn("dashboard", registry)
|
|
self.assertIn("file", registry)
|
|
self.assertIn("mariadb", registry)
|
|
self.assertIn("ollama", registry)
|
|
self.assertEqual(detect_service_channel("web-app-dashboard"), "frontend")
|
|
self.assertEqual(detect_service_channel("svc-db-mariadb"), "backend")
|
|
self.assertEqual(detect_service_bucket("web-app-dashboard"), "web-app")
|
|
self.assertEqual(detect_service_bucket("web-svc-file"), "web-svc")
|
|
self.assertEqual(detect_service_bucket("svc-db-mariadb"), "universal")
|
|
|
|
def test_discovery_passes_overlay_block_through(self):
|
|
applications = {
|
|
"svc-db-mariadb": {
|
|
"services": {"mariadb": {"enabled": True, "shared": True}},
|
|
"networks": {
|
|
"overlay": {
|
|
"modes": ["compose", "swarm"],
|
|
"topology": "shared_net",
|
|
"aliases": ["mariadb"],
|
|
"consumer": {"kind": "database"},
|
|
}
|
|
},
|
|
},
|
|
"svc-prx-openresty": {
|
|
"services": {"openresty": {"enabled": True, "shared": True}},
|
|
"networks": {
|
|
"overlay": {
|
|
"modes": ["swarm"],
|
|
"topology": "default_net",
|
|
"aliases": [],
|
|
"consumer": {
|
|
"kind": "services_flags",
|
|
"key": "sso",
|
|
"flags": ["enabled"],
|
|
},
|
|
}
|
|
},
|
|
},
|
|
"web-app-keycloak": {
|
|
"services": {
|
|
"keycloak": {
|
|
"enabled": True,
|
|
"shared": True,
|
|
"provides": "oidc",
|
|
}
|
|
},
|
|
"networks": {
|
|
"overlay": {
|
|
"modes": ["swarm"],
|
|
"proxy_resolvable": True,
|
|
"aliases": ["auth.example"],
|
|
}
|
|
},
|
|
},
|
|
"web-app-plain": {"services": {"plain": {"enabled": True, "shared": True}}},
|
|
}
|
|
|
|
registry = build_service_registry_from_applications(applications)
|
|
|
|
self.assertEqual(registry["mariadb"]["overlay"]["modes"], ["compose", "swarm"])
|
|
self.assertEqual(registry["mariadb"]["overlay"]["topology"], "shared_net")
|
|
self.assertEqual(registry["mariadb"]["overlay"]["consumer"]["kind"], "database")
|
|
self.assertEqual(registry["openresty"]["overlay"]["modes"], ["swarm"])
|
|
self.assertEqual(registry["openresty"]["overlay"]["aliases"], [])
|
|
self.assertTrue(registry["oidc"]["overlay"]["proxy_resolvable"])
|
|
self.assertNotIn("topology", registry["oidc"]["overlay"])
|
|
self.assertEqual(registry["oidc"]["overlay"]["aliases"], ["auth.example"])
|
|
self.assertNotIn("overlay", registry["plain"])
|
|
|
|
def test_direct_service_resolution_uses_role_local_names(self):
|
|
registry = {
|
|
"ldap": {"role": "svc-db-openldap"},
|
|
"mariadb": {"role": "svc-db-mariadb"},
|
|
}
|
|
config = {
|
|
"services": {
|
|
"ldap": {"enabled": True, "shared": True},
|
|
"mariadb": {"enabled": True, "shared": True},
|
|
"ignored": {"enabled": True, "shared": False},
|
|
}
|
|
}
|
|
|
|
self.assertEqual(
|
|
resolve_service_dependency_roles_from_config(config, registry),
|
|
["svc-db-openldap", "svc-db-mariadb"],
|
|
)
|
|
|
|
|
|
class TestServiceRegistryOrdering(unittest.TestCase):
|
|
"""Per, ``run_after`` lives on the role's primary entity at
|
|
``meta/services.yml.<primary_entity>.run_after``."""
|
|
|
|
def _mk_role(
|
|
self,
|
|
root: Path,
|
|
role: str,
|
|
primary_entity: str,
|
|
services_payload: dict,
|
|
*,
|
|
run_after: list[str] | None = None,
|
|
) -> None:
|
|
|
|
role_dir = root / role
|
|
meta_dir = role_dir / "meta"
|
|
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
services = dict(services_payload)
|
|
if run_after is not None:
|
|
primary = dict(services.get(primary_entity, {}) or {})
|
|
if run_after:
|
|
primary["run_after"] = list(run_after)
|
|
services[primary_entity] = primary
|
|
|
|
meta_dir.joinpath("services.yml").write_text(
|
|
dump_yaml_str(services),
|
|
encoding="utf-8",
|
|
)
|
|
meta_dir.joinpath("main.yml").write_text(
|
|
"galaxy_info: {}\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def test_ordered_entries_follow_bucket_order_and_run_after(self):
|
|
registry = {
|
|
"mariadb": {
|
|
"role": "svc-db-mariadb",
|
|
"bucket": "universal",
|
|
"deploy_type": "universal",
|
|
},
|
|
"file": {
|
|
"role": "web-svc-file",
|
|
"bucket": "web-svc",
|
|
"deploy_type": "server",
|
|
},
|
|
"asset": {
|
|
"role": "web-svc-asset",
|
|
"bucket": "web-svc",
|
|
"deploy_type": "server",
|
|
},
|
|
"matomo": {
|
|
"role": "web-app-matomo",
|
|
"bucket": "web-app",
|
|
"deploy_type": "server",
|
|
},
|
|
"dashboard": {
|
|
"role": "web-app-dashboard",
|
|
"bucket": "web-app",
|
|
"deploy_type": "server",
|
|
},
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
roles_dir = Path(td)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"svc-db-mariadb",
|
|
"mariadb",
|
|
{"mariadb": {"enabled": False, "shared": True}},
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-svc-file",
|
|
"file",
|
|
{"file": {"enabled": False, "shared": True}},
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-svc-asset",
|
|
"asset",
|
|
{"asset": {"enabled": False, "shared": True}},
|
|
run_after=["web-svc-file"],
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-app-matomo",
|
|
"matomo",
|
|
{"matomo": {"enabled": False, "shared": True}},
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-app-dashboard",
|
|
"dashboard",
|
|
{"dashboard": {"enabled": False, "shared": True}},
|
|
run_after=["web-app-matomo"],
|
|
)
|
|
|
|
ordered = ordered_primary_service_entries(registry, roles_dir)
|
|
|
|
self.assertEqual(
|
|
[entry["role"] for entry in ordered],
|
|
[
|
|
"svc-db-mariadb",
|
|
"web-svc-file",
|
|
"web-svc-asset",
|
|
"web-app-matomo",
|
|
"web-app-dashboard",
|
|
],
|
|
)
|
|
|
|
def test_run_after_orders_across_buckets(self):
|
|
registry = {
|
|
"file": {
|
|
"role": "web-svc-file",
|
|
"bucket": "web-svc",
|
|
"deploy_type": "server",
|
|
},
|
|
"mail": {
|
|
"role": "sys-svc-mail",
|
|
"bucket": "universal",
|
|
"deploy_type": "universal",
|
|
},
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
roles_dir = Path(td)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-svc-file",
|
|
"file",
|
|
{"file": {"enabled": False, "shared": True}},
|
|
run_after=["sys-svc-mail"],
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"sys-svc-mail",
|
|
"mail",
|
|
{"mail": {"enabled": False, "shared": True}},
|
|
)
|
|
|
|
ordered = ordered_primary_service_entries(registry, roles_dir)
|
|
|
|
self.assertEqual(
|
|
[entry["role"] for entry in ordered],
|
|
["sys-svc-mail", "web-svc-file"],
|
|
)
|
|
|
|
def test_circular_run_after_fails_hard(self):
|
|
registry = {
|
|
"file": {"role": "web-svc-file", "bucket": "web-svc"},
|
|
"asset": {"role": "web-svc-asset", "bucket": "web-svc"},
|
|
}
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
roles_dir = Path(td)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-svc-file",
|
|
"file",
|
|
{"file": {"enabled": False, "shared": True}},
|
|
run_after=["web-svc-asset"],
|
|
)
|
|
self._mk_role(
|
|
roles_dir,
|
|
"web-svc-asset",
|
|
"asset",
|
|
{"asset": {"enabled": False, "shared": True}},
|
|
run_after=["web-svc-file"],
|
|
)
|
|
|
|
with self.assertRaises(ServiceRegistryError):
|
|
ordered_primary_service_entries(registry, roles_dir)
|
|
|
|
|
|
class TestExplicitTruth(unittest.TestCase):
|
|
"""A flag may resolve a dependency through a group-gated constant.
|
|
|
|
The substring rule reads the role id out of the flag itself, so a SPOT
|
|
constant hides it. Roles referencing one keep their provider only while
|
|
the constant is registered.
|
|
"""
|
|
|
|
def test_a_literal_true_resolves(self):
|
|
self.assertTrue(is_explicit_truth(True))
|
|
|
|
def test_the_in_group_names_form_resolves(self):
|
|
self.assertTrue(is_explicit_truth("{{ 'web-app-keycloak' in group_names }}"))
|
|
|
|
def test_every_registered_constant_resolves(self):
|
|
for constant in GROUP_GATED_CONSTANTS:
|
|
with self.subTest(constant=constant):
|
|
self.assertTrue(is_explicit_truth("{{ " + constant + " }}"))
|
|
|
|
def test_an_unregistered_jinja_expression_does_not_resolve(self):
|
|
self.assertFalse(
|
|
is_explicit_truth("{{ lookup('config', 'web-app-gitea', 'mcp.enabled') }}")
|
|
)
|
|
|
|
def test_a_literal_false_does_not_resolve(self):
|
|
self.assertFalse(is_explicit_truth(False))
|
|
|
|
def test_every_registered_constant_is_defined_in_group_vars(self):
|
|
group_vars = PROJECT_ROOT / "group_vars" / "all"
|
|
defined = "".join(read_text(str(path)) for path in group_vars.glob("*.yml"))
|
|
for constant in GROUP_GATED_CONSTANTS:
|
|
with self.subTest(constant=constant):
|
|
self.assertIn(f"{constant}:", defined)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|