Files
computer-playbook/plugins/lookup/application_gid.py
kevinveenbirkenbachandClaude Opus 5 ef5ea7cb55 refactor(roles): name the credential file meta/secrets.yml
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>
2026-08-12 12:08:06 +02:00

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."
)