Files
computer-playbook/tests/unit/python/cli/administration/roles/_fixtures.py
T
kevinveenbirkenbachandClaude Opus 5.5 d92375922d feat(roles): pull custom roles from inventory-declared sources
An inventory group web-app-cst-<name> with applications.<id>.custom: true and
a pinned source {url, version, path} is pulled into roles/ before the play,
read raw (Jinja, Ansible tags and credential URLs refused), placed through a
staging directory under an exclusive lock, and owned by a marker that names
its own directory, so a pull or roles-clean never replaces or removes a
directory it did not place and a failed pull leaves roles/ as it was.
Directory inventories, host lists and non-UTF-8 files behave as Ansible
reads them; generated test deploys pass --skip-pull. make roles-pull,
roles-clean and roles list, INFINITO_EXTRA_MOUNTS for local sources, the
web-app-cst category, the reserved prefix lint and a central
repository_files() that also yields the gitignored custom roles let every
lint scan them like in-tree roles. Supersedes the uncommitted 045 External
Roles draft and reuses its fetch, sync and mount design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 00:02:58 +01:00

150 lines
4.3 KiB
Python

"""Throwaway role repositories, inventories and core roles/ trees for the pull tests."""
from __future__ import annotations
import json
import os
import subprocess
from typing import TYPE_CHECKING
from utils.roles.mapping import (
ROLE_FILE_META_MAIN,
ROLE_FILE_TASKS_MAIN,
ROLE_FILE_VARS_MAIN,
)
if TYPE_CHECKING:
from pathlib import Path
ROLE = "web-app-cst-demo"
ROLE_PATH = f"ansible/roles/{ROLE}"
_ENV = {
**{key: value for key, value in os.environ.items() if not key.startswith("GIT_")},
"GIT_CONFIG_GLOBAL": os.devnull,
}
def git(cwd: Path, *args: str) -> str:
"""Run git in *cwd* with a throwaway identity and no signing.
Args:
cwd: the repository.
args: the git subcommand and its arguments.
"""
return subprocess.run(
[
"git",
"-c",
"user.name=test",
"-c",
"user.email=test@example.invalid",
"-c",
"commit.gpgsign=false",
"-c",
"tag.gpgsign=false",
*args,
],
cwd=cwd,
env=_ENV,
capture_output=True,
text=True,
check=True,
).stdout
def write(path: Path, text: str) -> Path:
"""Write *text* to *path*, creating its parents.
Args:
path: the file.
text: its content.
"""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def role_repo(root: Path, application_id: str = ROLE) -> Path:
"""A repository whose tag ``v1.0.0`` holds the role and whose branch ``feature`` moved on.
Args:
root: directory the repository is created in.
application_id: the ``vars/main.yml`` application_id of the role.
"""
repo = root / "repo"
write(repo / "README.md", "repository root\n")
write(repo / ROLE_PATH / ROLE_FILE_META_MAIN, "galaxy_info: {}\n")
write(repo / ROLE_PATH / ROLE_FILE_VARS_MAIN, f"application_id: {application_id}\n")
write(repo / ROLE_PATH / ROLE_FILE_TASKS_MAIN, "release: v1\n")
git(repo, "init", "-q", "-b", "main")
git(repo, "add", "-A")
git(repo, "commit", "-q", "-m", "v1")
git(repo, "tag", "v1.0.0")
git(repo, "checkout", "-q", "-b", "feature")
write(repo / ROLE_PATH / ROLE_FILE_TASKS_MAIN, "release: feature\n")
git(repo, "commit", "-q", "-am", "feature")
return repo
def local_role(root: Path, group: str) -> Path:
"""A checkout without Git history that holds the role *group* under ansible/roles/.
Args:
root: directory the checkout is created in.
group: the role's group and application_id.
"""
checkout = root / f"{group}-checkout"
write(checkout / "ansible/roles" / group / ROLE_FILE_META_MAIN, "galaxy_info: {}\n")
write(
checkout / "ansible/roles" / group / ROLE_FILE_VARS_MAIN,
f"application_id: {group}\n",
)
return checkout
def inventory(root: Path, groups: list[str], applications: dict) -> Path:
"""An inventory whose host joins *groups* and whose host_vars set *applications*.
Args:
root: directory the inventory is created in.
groups: inventory groups ``localhost`` joins.
applications: the host_vars ``applications`` block.
"""
write(
root / "host_vars/localhost.yml",
json.dumps({"applications": applications}),
)
children = {group: {"hosts": {"localhost": {}}} for group in groups}
return write(root / "devices.yml", json.dumps({"all": {"children": children}}))
def declaration(
url: str, version: str | None = "v1.0.0", path: str = ROLE_PATH
) -> dict:
"""The ``applications.<group>`` block that declares a custom role.
Args:
url: ``source.url``.
version: ``source.version``; ``None`` leaves it out.
path: ``source.path``.
"""
source = {"url": url, "path": path}
if version is not None:
source["version"] = version
return {"custom": True, "source": source}
def roles_dir(root: Path, *in_tree: str) -> Path:
"""A core roles/ holding the given in-tree role directories.
Args:
root: directory the roles/ is created in.
in_tree: names of in-tree roles to create.
"""
roles = root / "roles"
roles.mkdir(parents=True)
for name in in_tree:
(roles / name).mkdir()
return roles