mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
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>
139 lines
4.3 KiB
Python
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:]))
|