Files
computer-playbook/plugins/lookup/application_gid.py
T
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).

* Document the rule in `docs/contributing/documentation.md` under
  "Comments". Code MUST describe the *why* in present-state language;
  contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
  `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
  `eslint.config.js`. Pure comment / docstring / log-message edits;
  no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
  enforce the rule going forward across `.js`, `.py`, `.yml`,
  `.yaml`, `.j2`, and `.sh` files. The legacy
  `test_no_stale_requirement_refs.py` is exempt as the only file
  whose purpose is to detect these very tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00

94 lines
3.8 KiB
Python

import os
from pathlib import Path
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.
Raises ``ValueError`` (not ``AnsibleError``) for portability.
"""
if not Path(roles_dir).is_dir():
raise ValueError(f"Roles directory '{roles_dir}' not found")
# Per, an "application role" is identified by the presence of
# at least one of the project-owned `meta/<topic>.yml` files (services,
# server, rbac, volumes, schema, users). This preserves the prior
# assignment ordering: every role that previously had `meta/services.yml`
# now has at least one of these files.
application_marker_files = {
"services.yml",
"server.yml",
"rbac.yml",
"volumes.yml",
"schema.yml",
"users.yml",
}
discovered: set[str] = set()
for entry in os.listdir(roles_dir):
role_dir = str(Path(roles_dir) / entry)
meta_dir = str(Path(role_dir) / "meta")
if not Path(meta_dir).is_dir():
continue
for marker in application_marker_files:
if Path(str(Path(meta_dir) / marker)).is_file():
discovered.add(entry)
break
sorted_ids = sorted(discovered)
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
# The Ansible LookupModule wrapper. We define it conditionally on
# ansible being importable: ansible's plugin loader only needs the class
# when an Ansible process actually loads this module via the lookup
# loader, and that always happens inside a process that has ansible
# installed (the playbook runner). Pure-Python importers (e.g.
# `utils.cache.applications._build_variants` running inside `cli.administration.deploy.
# development init` on the GitHub Actions runner host) want
# `compute_application_gid` only and MUST NOT pay the ansible-import
# cost — see CI run 24935979190 for the regression that motivated this
# split.
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."
)