"""Runtime identity of the PRODUCTION FIFA-17 stack, shared by the staging lifecycle scripts. This exists because both `sold-staging-up.py` and `sold-staging-down.py` need the same answer to the same safety question -- "what is production right now, and is it still healthy?" -- and two hand-maintained copies of a safety gate is two chances to rot. Production runs in containers, so its pids are NOT stable facts: every pid changes when a container is restarted. A hardcoded pid list decays into the worst of both worlds -- a kill-refusal gate that guards nothing (the real production pids are no longer in it) and a liveness gate that fails a perfectly good teardown (the pids it does list are long dead). So pids and published ports are both resolved from the container runtime at the moment they are needed. """ from __future__ import annotations import subprocess # Production containers. Anything running inside one of these is production. PROD_CONTAINERS = ("openfut-fut-backend", "openfut-bridge-1", "openfut-core-1") # Reserved ports: staging may never bind one of these, whether or not it is # currently published. 8199 (Python oracle) and 18080 (Core) belonged to the # retired host-process deployment and are kept so an old port map cannot be # silently reused by staging. PROD_PORTS = frozenset( {8080, 8081, 8085, 8094, 8099, 8199, 8443, 4216, 18080, 42127, 42130, 42131} ) class ProductionError(RuntimeError): """Production could not be observed, or is not healthy.""" def _inspect(container: str, template: str) -> str: """One `docker inspect -f` field. Any failure is fatal by design: a script that cannot see production must refuse to signal anything rather than assume the best. """ try: result = subprocess.run( ["docker", "inspect", "-f", template, container], capture_output=True, text=True, timeout=30, check=False, ) except (OSError, subprocess.SubprocessError) as exc: raise ProductionError( f"cannot inspect production container {container!r}: {exc}" ) from exc if result.returncode != 0: detail = result.stderr.strip() or f"docker exited {result.returncode}" raise ProductionError( f"cannot inspect production container {container!r}: {detail}" ) return result.stdout.strip() def listening_ports() -> set[int]: """Ports in state LISTEN, from the kernel socket table. A trial bind() would report EADDRINUSE for a stopped server's TIME_WAIT sockets and so wrongly claim a port is still served. """ ports: set[int] = set() for path in ("/proc/net/tcp", "/proc/net/tcp6"): try: with open(path) as fh: next(fh, None) # header for line in fh: fields = line.split() if len(fields) < 4 or fields[3] != "0A": # TCP_LISTEN continue ports.add(int(fields[1].rsplit(":", 1)[1], 16)) except OSError: continue return ports class ProductionState: """A snapshot of production as the container runtime reports it.""" __slots__ = ("pids", "published") def __init__(self, pids: dict[int, str], published: dict[int, str]) -> None: self.pids = pids self.published = published def describe(self) -> list[str]: return [f"{what} pid {pid}" for pid, what in sorted(self.pids.items())] def assert_serving(self) -> None: """Every port production publishes must actually be listening.""" listening = listening_ports() silent = sorted(port for port in self.published if port not in listening) if silent: raise ProductionError( "production port(s) no longer listening: " + ", ".join(f"{port} ({self.published[port]})" for port in silent) ) def assert_unchanged(self, before: "ProductionState") -> None: """Production must be the same processes serving the same ports.""" if self.pids != before.pids: raise ProductionError( f"production pids CHANGED: before={before.pids}, after={self.pids}" ) if set(self.published) != set(before.published): raise ProductionError( "production published ports CHANGED: " f"before={sorted(before.published)}, after={sorted(self.published)}" ) def production_state() -> ProductionState: """Resolve production's current pids and published ports, proving every production container is running.""" pids: dict[int, str] = {} published: dict[int, str] = {} for container in PROD_CONTAINERS: status = _inspect(container, "{{.State.Status}}") if status != "running": raise ProductionError( f"production container {container} is {status!r}, not running" ) pid = int(_inspect(container, "{{.State.Pid}}") or 0) if pid <= 0: raise ProductionError( f"production container {container} is running but reports no pid" ) pids[pid] = f"prod {container}" ports = _inspect( container, "{{range $port, $bindings := .NetworkSettings.Ports}}" "{{range $bindings}}{{.HostPort}} {{end}}{{end}}", ) for field in ports.split(): published[int(field)] = container return ProductionState(pids, published)