mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
meta/schema.yml held credential definitions and their runtime values, but the name said 'schema' and read like structural metadata next to services.yml and volumes.yml. Nothing in the file is a schema in the sense the other meta files use the word, and a contributor had to open it to learn it holds secrets. 62 role files renamed. ROLE_FILE_META_SCHEMA becomes ROLE_FILE_META_SECRETS and keeps its role as the SPOT the path is built from. Two readers bypassed that constant and spelled the name themselves: utils/cache/applications.py built meta_dir / 'schema.yml' twice, and plugins/lookup/application_gid.py carries the filename in the marker list it detects application roles by. Left behind, the rename would not have raised — credentials would have resolved empty and the role would have stopped counting as an application. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
3.5 KiB
Python
98 lines
3.5 KiB
Python
import functools
|
|
import os
|
|
from pathlib import Path
|
|
|
|
_APPLICATION_MARKER_FILES = (
|
|
"services.yml",
|
|
"server.yml",
|
|
"rbac.yml",
|
|
"volumes.yml",
|
|
"secrets.yml",
|
|
"users.yml",
|
|
)
|
|
|
|
|
|
@functools.cache
|
|
def _discover_sorted_application_ids(roles_dir_abs: str) -> tuple[str, ...]:
|
|
"""Sorted application role ids under *roles_dir_abs*, memoised per
|
|
process (utils.cache philosophy: CLI/test invocations rescan at most
|
|
once; a role added mid-process needs a fresh interpreter or
|
|
``_discover_sorted_application_ids.cache_clear()``).
|
|
|
|
An "application role" is identified by the presence of at least one
|
|
project-owned ``meta/<topic>.yml`` marker file.
|
|
"""
|
|
discovered: list[str] = []
|
|
with os.scandir(roles_dir_abs) as entries:
|
|
for entry in entries:
|
|
if not entry.is_dir():
|
|
continue
|
|
meta_dir = Path(entry.path) / "meta"
|
|
for marker in _APPLICATION_MARKER_FILES:
|
|
if (meta_dir / marker).is_file():
|
|
discovered.append(entry.name)
|
|
break
|
|
return tuple(sorted(discovered))
|
|
|
|
|
|
def compute_application_gid(application_id, roles_dir="roles", base_gid=10000):
|
|
"""Pure-Python GID resolver — no ansible dependency.
|
|
|
|
Sorts every `<roles_dir>/<role>/meta/services.yml`-bearing role
|
|
alphabetically by `<role>` and returns ``base_gid + index`` for the
|
|
requested ``application_id``. Extracted so callers that only need
|
|
the GID computation (e.g. ``utils.cache.applications._build_variants`` on
|
|
the GitHub Actions runner host, where the runner Python ships
|
|
without ``ansible``) can import THIS function instead of the
|
|
``LookupModule`` class — that one transitively pulls
|
|
``ansible.plugins.lookup.LookupBase`` and raises
|
|
``ModuleNotFoundError`` on ansible-less hosts.
|
|
|
|
An "application role" carries at least one project-owned
|
|
`meta/<topic>.yml` marker file (services, server, rbac, volumes,
|
|
schema, users).
|
|
|
|
Raises ``ValueError`` (not ``AnsibleError``) for portability.
|
|
"""
|
|
if not Path(roles_dir).is_dir():
|
|
raise ValueError(f"Roles directory '{roles_dir}' not found")
|
|
|
|
sorted_ids = _discover_sorted_application_ids(str(Path(roles_dir).resolve()))
|
|
|
|
try:
|
|
index = sorted_ids.index(application_id)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Application ID '{application_id}' not found in any role"
|
|
) from None
|
|
|
|
return base_gid + index
|
|
|
|
|
|
try:
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(self, terms, variables=None, **kwargs):
|
|
application_id = terms[0]
|
|
base_gid = kwargs.get("base_gid", 10000)
|
|
roles_dir = kwargs.get("roles_dir", "roles")
|
|
|
|
try:
|
|
return [compute_application_gid(application_id, roles_dir, base_gid)]
|
|
except ValueError as exc:
|
|
raise AnsibleError(str(exc)) from exc
|
|
|
|
except ImportError: # pragma: no cover - exercised on ansible-less hosts only
|
|
# Sentinel so callers that *try* to instantiate the lookup outside
|
|
# an Ansible process get a clear, actionable error instead of a
|
|
# confusing AttributeError or NameError.
|
|
class LookupModule: # type: ignore[no-redef]
|
|
def __init__(self, *args, **kwargs):
|
|
raise RuntimeError(
|
|
"plugins.lookup.application_gid.LookupModule requires "
|
|
"ansible at runtime. Use compute_application_gid() "
|
|
"directly for ansible-less code paths."
|
|
)
|