mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
The package cache was configured in six places at once: the nginx upstream map, the consumer compose override, pip.conf, .npmrc, the apt sources and the twenty ensure_proxy_repo calls that bootstrap Nexus each spelled the same hostnames, repository names and suites by hand. A new upstream meant editing five files and hoping none was missed, and the docs told contributors to do exactly that. A role now declares what it needs under cache: in its meta/networks.yml, and everything else is derived from it. svc-cache-package holds the general upstreams; web-svc-libretranslate holds its own. All nine derived files are generated and gitignored, so a second spelling of a hostname cannot exist. utils.cache.hosts reads the declarations without PyYAML, because the .env generator reaches them before any dependency is installed; it raises rather than truncate when a comment sits at column 0 inside the block, which would otherwise drop every declaration below it in silence. utils.cache.records owns the Nexus record format and the Ubuntu mirror resolution, so the dev stack and the role provision from one rendering. A build may only resolve a cached host it can verify: the plain-HTTP hosts are hijacked for every image, and the TLS hosts only for an image whose Dockerfile installs the frontend CA. Hijacking all of them would fail every pip and npm build with CERTIFICATE_VERIFY_FAILED. svc-cache-package deploys the cache and its TLS frontend by calling the same shell scripts the dev stack calls, rather than reimplementing them as tasks. The role is invokable through the new svc-cache category and inert until a host carries it. Verified: the twenty Nexus payloads, the fifteen nginx server blocks and all seven package-manager configs are byte-identical to the hand-written files they replace, and the new category adds one role to the deploy order while preserving the relative order of the other 180. The role has not been deployed yet.
194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
"""Render what the package cache needs beside its role, from the declarations.
|
|
|
|
The role deploys the frontend and owns the templates and their tuning; the dev
|
|
compose stack has no inventory to render them with and comes through here
|
|
instead. Every value still has one home: the hosts and repositories come from
|
|
the ``cache:`` declarations, the nginx tuning from the role's ``vars/main.yml``,
|
|
and only the backend address differs, because the dev stack reaches Nexus over
|
|
a compose network alias while the role reaches it by its configured container.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from utils.cache.hosts import (
|
|
host_records,
|
|
hosts_serving,
|
|
origin_of,
|
|
repo_named,
|
|
repo_records,
|
|
)
|
|
from utils.cache.yaml import load_yaml_any
|
|
from utils.distros import distro_names
|
|
from utils.roles.mapping import ROLE_FILE_META_VOLUMES, ROLE_FILE_VARS_MAIN
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
ROLE = "svc-cache-package"
|
|
ROLE_DIR = PROJECT_ROOT / "roles" / ROLE
|
|
ROLE_VARS = ROLE_DIR / ROLE_FILE_VARS_MAIN
|
|
TEMPLATE_DIRS = (ROLE_DIR / "templates", PROJECT_ROOT / "compose" / "templates")
|
|
UPSTREAMS_TEMPLATE = "upstreams.conf.j2"
|
|
CONSUMER_TEMPLATE = "cache-consumer.yml.jinja"
|
|
CONSUMER = "compose.cache-consumer.yml"
|
|
|
|
PYPI_REPO = "pypi-proxy"
|
|
NPM_REPO = "npm-proxy"
|
|
|
|
BACKEND = "http://package-cache:8081"
|
|
TEMPLATED = "{{"
|
|
|
|
|
|
def _mount_target(volume: str, service: str) -> str:
|
|
"""Return where *volume* is mounted on *service*, from meta/volumes.yml.
|
|
|
|
The role reads the same value through the ``volume`` lookup, which needs
|
|
an inventory; the dev stack has none and reads the declaration directly,
|
|
so the mount point still has one home.
|
|
|
|
Args:
|
|
volume: the key in meta/volumes.yml.
|
|
service: the service whose mount to return.
|
|
"""
|
|
declared = load_yaml_any(str(ROLE_DIR / ROLE_FILE_META_VOLUMES)) or {}
|
|
mounts = (declared.get(volume) or {}).get("mounts") or []
|
|
return next(
|
|
(
|
|
m["target"]
|
|
for m in mounts
|
|
if m.get("service") == service and m.get("target")
|
|
),
|
|
"",
|
|
)
|
|
|
|
|
|
def _tuning() -> dict[str, str]:
|
|
"""Return the role's plain nginx settings, keyed as the template names them.
|
|
|
|
A value the role builds from its inventory is skipped: it has no meaning
|
|
without one, and the dev stack supplies its own.
|
|
"""
|
|
loaded = load_yaml_any(str(ROLE_VARS), default_if_missing={}) or {}
|
|
return {
|
|
key: value
|
|
for key, value in loaded.items()
|
|
if key.startswith("CACHE_PACKAGE_") and TEMPLATED not in str(value)
|
|
}
|
|
|
|
|
|
def _render(template: str, **context) -> str:
|
|
env = Environment(
|
|
loader=FileSystemLoader([str(path) for path in TEMPLATE_DIRS]),
|
|
trim_blocks=True,
|
|
keep_trailing_newline=True,
|
|
autoescape=False, # noqa: S701 - nginx and compose config, not markup
|
|
)
|
|
return env.get_template(template).render(**context)
|
|
|
|
|
|
def upstreams_conf() -> str:
|
|
"""Return the nginx frontend configuration the declarations produce."""
|
|
return _render(
|
|
UPSTREAMS_TEMPLATE,
|
|
CACHE_PACKAGE_UPSTREAMS=list(host_records().values()),
|
|
CACHE_PACKAGE_REPOS=repo_records(),
|
|
CACHE_PACKAGE_BACKEND=BACKEND,
|
|
CACHE_PACKAGE_CERTS_DIR=_mount_target("frontend_certs", "frontend"),
|
|
**_tuning(),
|
|
)
|
|
|
|
|
|
def cache_consumer_yml() -> str:
|
|
"""Return the compose override that points every cached host at the frontend."""
|
|
return _render(CONSUMER_TEMPLATE, CACHE_PACKAGE_HOSTNAMES=list(host_records()))
|
|
|
|
|
|
def pip_conf() -> str:
|
|
"""Return the pip configuration pointing at the cached PyPI hosts."""
|
|
repo = repo_named(PYPI_REPO)
|
|
hosts = hosts_serving(PYPI_REPO)
|
|
origin = origin_of(repo) if repo else ""
|
|
index = next((entry for entry in hosts if entry.host == origin), None)
|
|
if index is None:
|
|
raise SystemExit(
|
|
f"no cache: host declares itself the origin of {PYPI_REPO}, so pip "
|
|
"has no index URL to point at"
|
|
)
|
|
return _render(
|
|
"pip.conf.jinja",
|
|
CACHE_PACKAGE_PYPI_INDEX=f"{index.scheme()}://{index.host}/simple/",
|
|
CACHE_PACKAGE_PYPI_HOSTS=[entry.host for entry in hosts],
|
|
)
|
|
|
|
|
|
def npmrc() -> str:
|
|
"""Return the npm configuration pointing at the cached registry."""
|
|
hosts = hosts_serving(NPM_REPO)
|
|
if not hosts:
|
|
raise SystemExit(f"no cache: host serves {NPM_REPO}, so npm has no registry")
|
|
registry = hosts[0]
|
|
return _render(
|
|
"npmrc.jinja",
|
|
CACHE_PACKAGE_NPM_REGISTRY=f"{registry.scheme()}://{registry.host}/",
|
|
)
|
|
|
|
|
|
def apt_list(distro: str) -> str:
|
|
"""Return one distribution's apt source, empty where it does not use apt.
|
|
|
|
Args:
|
|
distro: the value INFINITO_DISTRO carries.
|
|
"""
|
|
name = f"apt-{distro}"
|
|
repo = repo_named(name)
|
|
hosts = hosts_serving(name)
|
|
source = ""
|
|
if repo and hosts:
|
|
entry = hosts[0]
|
|
path = entry.path_of(name).rstrip("/")
|
|
source = f"{entry.scheme()}://{entry.host}{path}"
|
|
return _render(
|
|
"apt.list.jinja",
|
|
CACHE_PACKAGE_APT_SOURCE=source,
|
|
CACHE_PACKAGE_APT_SUITE=repo.distribution if repo else "",
|
|
)
|
|
|
|
|
|
def _write(target: Path, content: str) -> Path:
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(content, encoding="utf-8")
|
|
return target
|
|
|
|
|
|
def render_artifacts(repo_root: Path, conf_path: Path) -> list[Path]:
|
|
"""Write every generated cache file and return what was written.
|
|
|
|
A worktree configured to follow the primary checkout reads that
|
|
checkout's map without writing it: rendering there would push this
|
|
branch's declarations into another tree, which is the isolation the
|
|
setting exists to provide.
|
|
|
|
Args:
|
|
repo_root: the checkout being generated for.
|
|
conf_path: where INFINITO_CACHE_PACKAGE_FRONTEND_CONF points.
|
|
"""
|
|
consumer_dir = repo_root / "compose" / "package-cache"
|
|
written = [
|
|
_write(repo_root / CONSUMER, cache_consumer_yml()),
|
|
_write(consumer_dir / "pip.conf", pip_conf()),
|
|
_write(consumer_dir / "npmrc", npmrc()),
|
|
]
|
|
written += [
|
|
_write(consumer_dir / "apt" / f"{distro}.list", apt_list(distro))
|
|
for distro in distro_names()
|
|
]
|
|
if conf_path.is_relative_to(repo_root):
|
|
written.append(_write(conf_path, upstreams_conf()))
|
|
return written
|