Files
computer-playbook/utils/cleanup/nginx_vhosts.py
T
kevinveenbirkenbachandClaude Opus 4.7 820fb0a222 fix(purge): clear stale nginx vhosts between matrix-deploy rounds
Matrix-deploy variant transitions left per-domain vhost files behind:
when a service dropped out of a round's include set, sys-svc-compose's
docker prune removed its container but the vhost under
<NGINX_DIR>/conf.d/servers/{http,https}/<domain>.conf survived, making
the next round's HTTPS probe see 502 instead of TLS-error 000 (5 failed
test-development jobs on run 25705903504).

The inter-round purge in cli.administration.deploy.development.deploy
already invokes scripts/tests/deploy/local/purge/entity.sh with the
previous round's full include — the missing piece was an entity-keyed
nginx primitive next to db/compose/dir. Add it:

- utils.roles.entity_apps.apps_for_entity: reverse of get_entity_name,
  walks roles/*/vars/main.yml to map entity -> [application_id]
- utils.cleanup.nginx_vhosts: resolves entity -> apps -> canonical+alias
  domains (via build_applications_from_roles + iter_app_domains so it
  stays ansible-free) and removes the matching vhost files
- scripts/container/purge/entity/nginx.sh: thin bash wrapper,
  symmetric to db/compose/dir
- scripts/container/purge/apps.sh: drive nginx.sh inside the entity
  loop, alongside db/compose/dir

Tests: 14 new unit tests covering the reverse-lookup edge cases and
the purge iterator (existing-only / aliases / multi-entity / no-op CLI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00

139 lines
4.3 KiB
Python

"""nginx vhost-file purge helper (entity-keyed).
Matrix-deploy variant transitions can leave per-domain nginx vhost
configs behind: when a service drops out of a round's include set,
``sys-svc-compose``'s aggressive docker prune removes the container,
but the vhost file under
``<NGINX_DIR>/conf.d/servers/{http,https}/<domain>.conf`` survives.
The next round's probe then gets HTTP 502 (nginx serves the vhost with
no upstream) instead of the expected TLS-error 000.
The entity-keyed purge primitive
``scripts/container/purge/entity/nginx.sh`` (driven by the app-keyed
orchestrator ``apps.sh`` as a sibling of ``db.sh``, ``compose.sh``,
``dir.sh``) invokes this module to remove the vhost files belonging to
a given compose entity. The mapping entity -> apps -> domains is
resolved via
``utils.roles.entity_apps.apps_for_entity`` plus
``utils.domains.list.build_applications_from_roles`` /
``utils.domains.application_domain_index.iter_app_domains`` so the
helper stays ansible-free and runnable inside the infinito container
without a templar context.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from utils.domains.application_domain_index import iter_app_domains
from utils.domains.list import ROLES_DIR, build_applications_from_roles
from utils.roles.entity_apps import apps_for_entity
if TYPE_CHECKING:
from collections.abc import Iterable
DEFAULT_NGINX_DIR = Path("/etc/nginx")
DEFAULT_DOMAIN_PRIMARY = "infinito.example"
_PROTOCOLS: tuple[str, ...] = ("http", "https")
def _resolve_nginx_dir(nginx_dir: Path | None) -> Path:
if nginx_dir is not None:
return nginx_dir
env_value = os.environ.get("NGINX_DIR")
return Path(env_value) if env_value else DEFAULT_NGINX_DIR
def _resolve_domain_primary(domain_primary: str | None) -> str:
if domain_primary is not None:
return domain_primary
return os.environ.get("DOMAIN", DEFAULT_DOMAIN_PRIMARY)
def iter_vhost_files_for_entity(
entity: str,
*,
nginx_dir: Path,
domain_primary: str,
roles_dir: Path = ROLES_DIR,
) -> Iterable[Path]:
"""Yield existing vhost files (http + https) for every app belonging
to *entity*. Only yields files that exist on disk.
"""
apps = apps_for_entity(entity, roles_dir=roles_dir)
if not apps:
return
applications = build_applications_from_roles(roles_dir, domain_primary)
servers_dir = nginx_dir / "conf.d" / "servers"
for app_id in apps:
app_conf = applications.get(app_id)
if not app_conf:
continue
for domain in iter_app_domains(app_conf, include_aliases=True):
for proto in _PROTOCOLS:
conf_file = servers_dir / proto / f"{domain}.conf"
if conf_file.is_file():
yield conf_file
def purge_vhost_files_for_entities(
entities: list[str],
*,
nginx_dir: Path | None = None,
domain_primary: str | None = None,
roles_dir: Path = ROLES_DIR,
) -> list[Path]:
"""Remove vhost files for every entity in *entities*. Returns the
list of paths that were actually removed (existed and unlink
succeeded), in iteration order.
"""
nd = _resolve_nginx_dir(nginx_dir)
dp = _resolve_domain_primary(domain_primary)
removed: list[Path] = []
for entity in entities:
for path in iter_vhost_files_for_entity(
entity,
nginx_dir=nd,
domain_primary=dp,
roles_dir=roles_dir,
):
try:
path.unlink()
except FileNotFoundError:
continue
except OSError as exc:
print(
f"!!! WARNING: failed to remove {path}: {exc}",
file=sys.stderr,
)
continue
removed.append(path)
return removed
def main(argv: list[str]) -> int:
if not argv:
print(
"usage: python -m utils.cleanup.nginx_vhosts <ENTITY> [ENTITY ...]",
file=sys.stderr,
)
return 2
removed = purge_vhost_files_for_entities(argv)
if removed:
for path in removed:
print(f">>> removed {path}")
else:
print(">>> No nginx vhost files to remove for entities: " + ", ".join(argv))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))