#!/usr/bin/env python3 """Resolve the live Prod route and the next physical backend slot.""" from __future__ import annotations import re from dataclasses import dataclass PROD_SERVER_NAME = "nexuscrm-new-prod.pingcap.com" PROD_D3_MARKER = "# NexusCRM D3 direct ingress" PROD_BACKEND_PORTS = (3103, 3104, 3105, 3106) PROD_SLOT_SEQUENCE = tuple(f"slot-{port}" for port in PROD_BACKEND_PORTS) PROD_SLOT_PORTS = dict(zip(PROD_SLOT_SEQUENCE, PROD_BACKEND_PORTS)) PROD_D3_PUBLIC_PROXY_PORTS = { "slot-3103": frozenset({3103, 3204, 3403}), "slot-3104": frozenset({3104, 3205, 3413}), "slot-3105": frozenset({3105, 3206, 3423}), "slot-3106": frozenset({3106, 3207, 3433}), } class IngressError(ValueError): pass @dataclass(frozen=True) class ProdRoute: backend_port: int slot: str is_d3: bool def _server_block_bounds(content: str, server_name: str) -> tuple[int, int]: matches: list[tuple[int, int]] = [] for match in re.finditer(r"(?m)^\s*server\s*\{\s*$", content): depth = 0 end = -1 for index in range(match.start(), len(content)): if content[index] == "{": depth += 1 elif content[index] == "}": depth -= 1 if depth == 0: end = index + 1 break if end < 0: raise IngressError("Prod nginx contains an unterminated server block") block = content[match.start() : end] names = re.findall(r"(?m)^\s*server_name\s+([^;]+);", block) if any(server_name in value.split() for value in names): matches.append((match.start(), end)) if len(matches) != 1: raise IngressError( f"Prod nginx must contain exactly one server block for {server_name}; " f"found {len(matches)}" ) return matches[0] def parse_prod_route(content: str) -> ProdRoute: start, end = _server_block_bounds(content, PROD_SERVER_NAME) block = content[start:end] proxy_targets = re.findall(r"\bproxy_pass\s+([^;\s]+)\s*;", block) all_upstreams: set[int] = set() for target in proxy_targets: match = re.fullmatch( r"http://127\.0\.0\.1:([0-9]+)(?:/[^\s;]*)?", target ) if not match: raise IngressError( f"Prod nginx contains an unreviewed proxy target: {target}" ) all_upstreams.add(int(match.group(1))) backend_upstreams = all_upstreams.intersection(PROD_BACKEND_PORTS) if len(backend_upstreams) != 1: raise IngressError( "Prod nginx has no unique backend port in the reviewed slot set" ) backend_port = next(iter(backend_upstreams)) slot = slot_for_port(backend_port) marked = PROD_D3_MARKER in content[max(0, start - 160) : end] # Classic's HubSpot adapter also uses backend + 101 (see # deploy/online/runtime/common.py:hubspot_options_adapter_port). Its # presence does not imply D3 ownership or permit a D3 Integration route. allowed = ( PROD_D3_PUBLIC_PROXY_PORTS[slot] if marked else {backend_port, backend_port + 101} ) unexpected = all_upstreams.difference(allowed) if unexpected: raise IngressError( "Prod nginx contains proxy targets outside the routed slot: " + ", ".join(str(port) for port in sorted(unexpected)) ) return ProdRoute(backend_port=backend_port, slot=slot, is_d3=marked) def slot_for_port(port: int) -> str: try: return PROD_SLOT_SEQUENCE[PROD_BACKEND_PORTS.index(port)] except ValueError as exc: raise IngressError(f"unknown Prod backend port: {port}") from exc def slot_port(slot: str) -> int: try: return PROD_SLOT_PORTS[slot] except KeyError as exc: raise IngressError(f"unknown Prod backend slot: {slot}") from exc def next_slot(route: ProdRoute) -> str: """Return the physical successor; blue/green are per-release roles only.""" try: index = PROD_SLOT_SEQUENCE.index(route.slot) except ValueError as exc: raise IngressError(f"unknown Prod route slot: {route.slot}") from exc return PROD_SLOT_SEQUENCE[(index + 1) % len(PROD_SLOT_SEQUENCE)]