mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
`config/main.yml` exists in no role: f65f4d60fb migrated the last one. A guard
against the string already exists, `TestNoLegacyPathReferences`, and it names
`config/main.yml` among its LEGACY_TOKENS. It scanned plugins, utils, cli,
roles, scripts, filter_plugins and lookup_plugins, which is every tree except
the two the stale references actually lived in.
So nine of them survived in `tests/`, and the guard reported green the whole
time. The code underneath was already correct everywhere: `test_csp_consistency`
and `test_role_wiring` iterate ROLE_FILE_META_SERVICES, and only their docstrings
and one assertion message still said `config/main.yml`. Had any of them globbed
the old path the test would have gone vacuous rather than red, which is the
failure mode worth naming here.
`tests` and `tasks` are now scanned. The targets come from
`utils/roles/mapping.py`, not from guesswork: `config/main.yml` is
`meta/services.yml`, `schema/main.yml` is `meta/secrets.yml` (confirmed at
`cli/administration/inventory/provision/reset.py:67`, which reads exactly that
file), and `users/main.yml` is `meta/users.yml`.
Scanning `tasks` exposed a second dead pointer. The migration tool became a
package on 2026-09-14, but EXEMPT_FILES still named `migrate_meta_layout.py`, a
file that no longer exists, so the exemption was inert and only invisible
because `tasks` went unscanned. Two dead pointers covering for each other. The
exemption is now a directory, so a new module in that package cannot fall out of
it. `test_no_direct_calls.py` named the same dead path in prose; that lint
allows through nocheck markers at the call site, so nothing was broken there.
`docs/administration/configuration/email.md` was the worst of them, because it
instructed operators rather than describing tests: it told them to write
`compose.services.email` into `roles/<application_id>/config/main.yml`. The
consumer, `plugins/lookup/email.py:255`, reads `entry["services"]["email"]` and
says in its own comment that there is no `compose.services` envelope. The role
snippet, the inventory snippet and the precedence list now match the reader.
`group_vars/all/TODO.md` pointed at the same dead file.
Verified by watching the guard fail, twice. Re-introducing `config/main.yml` in
a test file reports it; pointing the new directory exemption at a wrong path
reports `tasks/utils/migrate_meta_layout/credentials.py`, which proves both that
`tasks` is scanned and that the exemption is the only thing letting the
migration tool through. Restored, 11 passed.
The changelogs under `docs/changelog/` keep their mentions, and `.md` stays out
of the scanned suffixes for that reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
230 lines
8.2 KiB
Python
230 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from plugins.lookup.native_metrics_apps import LookupModule
|
|
|
|
|
|
def _run(
|
|
applications: dict,
|
|
roles_dir: Path,
|
|
group_names: list | None = None,
|
|
deployment_mode: str = "compose",
|
|
) -> list:
|
|
"""Helper: run the lookup with a temporary roles directory.
|
|
|
|
group_names defaults to all keys in applications so existing tests that
|
|
don't care about the deployment filter still pass without change.
|
|
"""
|
|
if group_names is None:
|
|
group_names = list(applications.keys())
|
|
lm = LookupModule()
|
|
lm._loader = mock.MagicMock()
|
|
lm._templar = mock.MagicMock(template=lambda v: v)
|
|
with (
|
|
patch("plugins.lookup.native_metrics_apps.ROLES_DIR", roles_dir),
|
|
patch("plugins.lookup.native_metrics_apps.lookup_loader") as loader_mock,
|
|
):
|
|
|
|
def _get(name, *a, **k):
|
|
if name == "applications":
|
|
return mock.MagicMock(run=lambda *_a, **_k: [applications])
|
|
return mock.MagicMock(run=lambda *_a, **_k: [{}])
|
|
|
|
loader_mock.get.side_effect = _get
|
|
return lm.run(
|
|
[],
|
|
variables={
|
|
"applications": applications,
|
|
"group_names": group_names,
|
|
"DEPLOYMENT_MODE": deployment_mode,
|
|
},
|
|
)[0]
|
|
|
|
|
|
def _make_roles(tmp: Path, specs: dict) -> dict:
|
|
"""Create minimal role fixtures under tmp/ and return an applications dict.
|
|
|
|
In real Ansible, role meta/services.yml defaults are merged into the
|
|
`applications` variable before any play runs. This helper replicates that
|
|
by building both the filesystem structure (for fragment discovery) and the
|
|
applications dict (for get_app_conf lookups) from the same spec.
|
|
|
|
specs: { app_id: {"native_metrics_enabled": bool, "has_fragment": bool,
|
|
"force_bridge": bool} }
|
|
Returns: applications dict ready to pass to the lookup.
|
|
"""
|
|
applications = {}
|
|
for app_id, cfg in specs.items():
|
|
if cfg.get("has_fragment"):
|
|
tpl_dir = tmp / app_id / "templates"
|
|
tpl_dir.mkdir(parents=True)
|
|
(tpl_dir / "prometheus.yml.j2").write_text(f' - job_name: "{app_id}"\n')
|
|
applications[app_id] = {
|
|
"networks": {"local": {"force_bridge": bool(cfg.get("force_bridge"))}},
|
|
"services": {
|
|
"prometheus": {
|
|
"native_metrics": {
|
|
"enabled": bool(cfg.get("native_metrics_enabled"))
|
|
}
|
|
}
|
|
},
|
|
}
|
|
return applications
|
|
|
|
|
|
class TestNativeMetricsApps(unittest.TestCase):
|
|
def setUp(self):
|
|
self._tmpdir = tempfile.TemporaryDirectory()
|
|
self.roles_dir = Path(self._tmpdir.name)
|
|
|
|
def tearDown(self):
|
|
self._tmpdir.cleanup()
|
|
|
|
# ── basic inclusion ────────────────────────────────────────────────────
|
|
|
|
def test_returns_app_with_enabled_and_fragment(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
|
|
},
|
|
)
|
|
self.assertEqual(_run(apps, self.roles_dir), ["web-app-gitea"])
|
|
|
|
def test_excludes_app_without_native_metrics_enabled(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-foo": {"native_metrics_enabled": False, "has_fragment": True},
|
|
},
|
|
)
|
|
self.assertEqual(_run(apps, self.roles_dir), [])
|
|
|
|
def test_excludes_app_without_scrape_fragment(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-bar": {"native_metrics_enabled": True, "has_fragment": False},
|
|
},
|
|
)
|
|
self.assertEqual(_run(apps, self.roles_dir), [])
|
|
|
|
def test_excludes_app_not_in_applications(self):
|
|
"""Apps with a fragment but not deployed must be excluded."""
|
|
_make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
|
|
},
|
|
)
|
|
self.assertEqual(_run({}, self.roles_dir), [])
|
|
|
|
# ── ordering ───────────────────────────────────────────────────────────
|
|
|
|
def test_result_is_sorted(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-zzz": {"native_metrics_enabled": True, "has_fragment": True},
|
|
"web-app-aaa": {"native_metrics_enabled": True, "has_fragment": True},
|
|
"web-app-mmm": {"native_metrics_enabled": True, "has_fragment": True},
|
|
},
|
|
)
|
|
self.assertEqual(
|
|
_run(apps, self.roles_dir), ["web-app-aaa", "web-app-mmm", "web-app-zzz"]
|
|
)
|
|
|
|
# ── multiple apps mixed ────────────────────────────────────────────────
|
|
|
|
def test_returns_only_qualifying_apps(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
|
|
"web-app-mattermost": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": True,
|
|
},
|
|
"web-app-matrix": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": True,
|
|
},
|
|
"web-app-nofrag": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": False,
|
|
},
|
|
"web-app-disabled": {
|
|
"native_metrics_enabled": False,
|
|
"has_fragment": True,
|
|
},
|
|
},
|
|
)
|
|
self.assertEqual(
|
|
_run(apps, self.roles_dir),
|
|
["web-app-gitea", "web-app-matrix", "web-app-mattermost"],
|
|
)
|
|
|
|
# ── group_names deployment filter ──────────────────────────────────────
|
|
|
|
def test_excludes_app_not_in_group_names(self):
|
|
"""Apps not in group_names (not deployed on this host) must be excluded."""
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
|
|
"web-app-mattermost": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": True,
|
|
},
|
|
},
|
|
)
|
|
result = _run(apps, self.roles_dir, group_names=["web-app-gitea"])
|
|
self.assertIn("web-app-gitea", result)
|
|
self.assertNotIn("web-app-mattermost", result)
|
|
|
|
def test_returns_empty_when_group_names_empty(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True}},
|
|
)
|
|
self.assertEqual(_run(apps, self.roles_dir, group_names=[]), [])
|
|
|
|
# ── force_bridge reachability (swarm only) ─────────────────────────────
|
|
|
|
def test_excludes_force_bridge_app_in_swarm(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-matrix": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": True,
|
|
"force_bridge": True,
|
|
},
|
|
},
|
|
)
|
|
self.assertEqual(_run(apps, self.roles_dir, deployment_mode="swarm"), [])
|
|
|
|
def test_includes_force_bridge_app_in_compose(self):
|
|
apps = _make_roles(
|
|
self.roles_dir,
|
|
{
|
|
"web-app-matrix": {
|
|
"native_metrics_enabled": True,
|
|
"has_fragment": True,
|
|
"force_bridge": True,
|
|
},
|
|
},
|
|
)
|
|
self.assertEqual(
|
|
_run(apps, self.roles_dir, deployment_mode="compose"), ["web-app-matrix"]
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|