Files
computer-playbook/utils/cache/inventory.py
T
kevinveenbirkenbach 230d9a1e5e feat(cache): declare every cached upstream once and generate the rest
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.
2026-09-27 16:28:48 +02:00

66 lines
2.3 KiB
Python

"""Apply a host's inventory to the cache declarations the roles make.
The dev compose stack has no inventory and caches exactly what the roles
declare. A deployed host caches what its configuration says, which starts
from the same declarations and lets the inventory correct or extend them::
applications:
svc-cache-package:
repos:
raw-example:
upstream: https://mirror.example.net/
upstreams:
example.org:
read_timeout: 1800s
Both sections are merged per entry, so an inventory that names one field
keeps the rest of the declaration, and a key nobody declared is added.
"""
from __future__ import annotations
from typing import Any
ROLE = "svc-cache-package"
def configured(loader: Any, templar: Any, variables: Any, section: str) -> dict:
"""Return the inventory's entries for one ``cache:`` section.
Args:
loader: the lookup plugin's loader.
templar: the lookup plugin's templar, where it has one.
variables: the task variables the lookup was called with.
section: ``repos`` or ``upstreams``.
Returns:
The configured entries, or an empty mapping where no inventory
answers, as in the dev stack.
"""
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
try:
applications = lookup_loader.get(
"applications", loader=loader, templar=templar
).run([], variables=variables or {})[0]
except (AnsibleError, KeyError, TypeError):
return {}
role = applications.get(ROLE) if isinstance(applications, dict) else None
found = (role or {}).get(section) if isinstance(role, dict) else None
return found if isinstance(found, dict) else {}
def merge(declared: dict[str, dict], overrides: dict) -> dict[str, dict]:
"""Return *declared* with *overrides* applied, sorted by key.
Args:
declared: the entries the roles declare, keyed by name.
overrides: the inventory's entries for the same section.
"""
merged = {key: dict(value) for key, value in declared.items()}
for key, entry in (overrides or {}).items():
if isinstance(entry, dict):
merged.setdefault(str(key), {}).update(entry)
return {key: merged[key] for key in sorted(merged)}