#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys from pathlib import Path ONLINE_RUNTIME_DIR = Path(__file__).resolve().parents[1] / "deploy" / "online" / "runtime" if str(ONLINE_RUNTIME_DIR) not in sys.path: sys.path.insert(0, str(ONLINE_RUNTIME_DIR)) import hubspot_prod_contract # noqa: E402 ORDINARY_MODES = {"block", "proxy", "ack_drop"} WORKFLOW_MODES = {"block", "proxy"} ORDINARY_LOCATION = "^~ /webhooks/hubspot-crm-connector/" GENERIC_WEBHOOK_LOCATION = "/webhooks/" WORKFLOW_EXECUTE_LOCATION = "= /hubspot/workflow-actions/execute" class ReadbackError(RuntimeError): pass def load_env_file(path: Path) -> dict[str, str]: if not path.is_file(): raise ReadbackError(f"missing active runtime env: {path}") values: dict[str, str] = {} for line_number, raw_line in enumerate(path.read_text().splitlines(), start=1): line = raw_line.strip() if not line or line.startswith("#"): continue if "=" not in line: raise ReadbackError(f"invalid runtime env line {line_number}: missing '='") key, value = line.split("=", 1) key = key.strip() value = value.strip() if not key: raise ReadbackError(f"invalid runtime env line {line_number}: empty key") if key in values: raise ReadbackError(f"duplicate runtime env key on line {line_number}: {key}") if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: value = value[1:-1] values[key] = value return values def extract_location_blocks(rendered: str) -> dict[str, str]: blocks: dict[str, str] = {} lines = rendered.splitlines() index = 0 while index < len(lines): stripped = lines[index].strip() match = re.fullmatch(r"location\s+(.+?)\s*\{", stripped) if match is None: index += 1 continue selector = " ".join(match.group(1).split()) if selector in blocks: raise ReadbackError(f"duplicate nginx location: {selector}") block_lines = [lines[index]] depth = lines[index].count("{") - lines[index].count("}") index += 1 while index < len(lines) and depth > 0: block_lines.append(lines[index]) depth += lines[index].count("{") - lines[index].count("}") index += 1 if depth != 0: raise ReadbackError(f"unterminated nginx location: {selector}") blocks[selector] = "\n".join(block_lines) return blocks def require_location(blocks: dict[str, str], selector: str) -> str: try: return blocks[selector] except KeyError as exc: raise ReadbackError(f"missing nginx location: {selector}") from exc def require_return_only(block: str, selector: str, status: int) -> None: expected = f"return {status};" return_lines = [ line.strip() for line in block.splitlines() if re.match(r"^\s*return\s+", line) ] if return_lines != [expected]: raise ReadbackError( f"nginx location {selector} return mismatch: " f"expected {[expected]!r}, got {return_lines!r}" ) if "proxy_pass" in block: raise ReadbackError(f"nginx location {selector} mixes return and proxy_pass") def require_proxy_only(block: str, selector: str, port: str) -> None: expected = f"proxy_pass http://127.0.0.1:{port};" proxy_lines = [line.strip() for line in block.splitlines() if "proxy_pass" in line] if proxy_lines != [expected]: raise ReadbackError( f"nginx location {selector} proxy mismatch: expected {expected!r}, got {proxy_lines!r}" ) if re.search(r"^\s*return\s+", block, re.MULTILINE): raise ReadbackError(f"nginx location {selector} mixes proxy_pass and return") def verify_readback( runtime_env_file: Path, nginx_conf: Path, *, expected_ordinary_mode: str, expected_workflow_mode: str, expected_workflow_action_id: str, ) -> dict[str, str]: expected_ordinary_mode = expected_ordinary_mode.strip().lower() expected_workflow_mode = expected_workflow_mode.strip().lower() if expected_ordinary_mode not in ORDINARY_MODES: raise ReadbackError(f"invalid expected ordinary mode: {expected_ordinary_mode!r}") if expected_workflow_mode not in WORKFLOW_MODES: raise ReadbackError(f"invalid expected workflow mode: {expected_workflow_mode!r}") envv = load_env_file(runtime_env_file) active_ordinary_mode = envv.get("HUBSPOT_ORDINARY_WEBHOOK_MODE", "").strip().lower() active_workflow_mode = envv.get("HUBSPOT_WORKFLOW_EXECUTE_MODE", "").strip().lower() if active_ordinary_mode != expected_ordinary_mode: raise ReadbackError( "active HUBSPOT_ORDINARY_WEBHOOK_MODE mismatch: " f"expected {expected_ordinary_mode!r}, got {active_ordinary_mode!r}" ) if active_workflow_mode != expected_workflow_mode: raise ReadbackError( "active HUBSPOT_WORKFLOW_EXECUTE_MODE mismatch: " f"expected {expected_workflow_mode!r}, got {active_workflow_mode!r}" ) expected_workflow_action_id = expected_workflow_action_id.strip() active_workflow_action_id = envv.get("EXP_HUBSPOT_WORKFLOW_ACTION_ID", "").strip() if not expected_workflow_action_id: raise ReadbackError("expected workflow action ID must not be empty") if active_workflow_action_id != expected_workflow_action_id: raise ReadbackError( "active EXP_HUBSPOT_WORKFLOW_ACTION_ID mismatch: " f"expected {expected_workflow_action_id!r}, got {active_workflow_action_id!r}" ) raw_workflow_policies = envv.get( "EXP_HUBSPOT_WORKFLOW_ACTION_POLICIES_JSON", "" ).strip() try: active_workflow_policies = json.loads(raw_workflow_policies) except json.JSONDecodeError as exc: raise ReadbackError( "active EXP_HUBSPOT_WORKFLOW_ACTION_POLICIES_JSON is not valid JSON" ) from exc expected_workflow_policies = {expected_workflow_action_id: "must_complete"} if active_workflow_policies != expected_workflow_policies: raise ReadbackError( "active workflow action delivery policy mismatch: " f"expected {expected_workflow_policies!r}, got {active_workflow_policies!r}" ) if not envv.get( "EXP_HUBSPOT_WORKFLOW_ACTION_CALLBACK_ACCESS_TOKEN", "" ).strip(): raise ReadbackError( "active workflow action callback access token is not configured" ) multi_prod_root = envv.get("MULTI_PROD_ROOT", "").strip() if not multi_prod_root: raise ReadbackError("active MULTI_PROD_ROOT must not be empty") expected_queue_db = str( Path(multi_prod_root).expanduser() / "shared" / "hubspot-workflow-action-queue.sqlite3" ) active_queue_db = envv.get("EXP_HUBSPOT_WORKFLOW_ACTION_QUEUE_DB", "").strip() if active_queue_db != expected_queue_db: raise ReadbackError( "active workflow action queue path mismatch: " f"expected {expected_queue_db!r}, got {active_queue_db!r}" ) active_replay_rps = envv.get("EXP_HUBSPOT_WORKFLOW_ACTION_REPLAY_RPS", "").strip() if active_replay_rps != "4": raise ReadbackError( "active workflow action replay RPS mismatch: " f"expected '4', got {active_replay_rps!r}" ) ack_drop_endpoint_key = "NEXUSCRM_CONNECTOR_WEBHOOK_ACK_DROP_ENDPOINTS" if ack_drop_endpoint_key not in envv: raise ReadbackError( "active NEXUSCRM_CONNECTOR_WEBHOOK_ACK_DROP_ENDPOINTS is missing" ) active_ack_drop_endpoints = envv[ack_drop_endpoint_key].strip() approved_ack_drop_endpoints = ( hubspot_prod_contract.PROD_CONNECTOR_WEBHOOK_ACK_DROP_ENDPOINTS ) if active_ack_drop_endpoints != approved_ack_drop_endpoints: raise ReadbackError( "active NEXUSCRM_CONNECTOR_WEBHOOK_ACK_DROP_ENDPOINTS mismatch: " f"expected {approved_ack_drop_endpoints!r}, " f"got {active_ack_drop_endpoints!r}" ) active_ack_drop_timeout_ms = envv.get( "NEXUSCRM_CONNECTOR_WEBHOOK_ACK_DROP_TIMEOUT_MS", "" ).strip() approved_ack_drop_timeout_ms = ( hubspot_prod_contract.PROD_CONNECTOR_WEBHOOK_ACK_DROP_TIMEOUT_MS ) if active_ack_drop_timeout_ms != approved_ack_drop_timeout_ms: raise ReadbackError( "active NEXUSCRM_CONNECTOR_WEBHOOK_ACK_DROP_TIMEOUT_MS mismatch: " f"expected {approved_ack_drop_timeout_ms!r}, " f"got {active_ack_drop_timeout_ms!r}" ) backend_port = envv.get("BACKEND_PORT", "").strip() adapter_port = envv.get("EXP_HUBSPOT_OPTIONS_ADAPTER_PORT", "").strip() if re.fullmatch(r"[1-9][0-9]*", backend_port) is None: raise ReadbackError(f"invalid active BACKEND_PORT: {backend_port!r}") if re.fullmatch(r"[1-9][0-9]*", adapter_port) is None: raise ReadbackError(f"invalid active EXP_HUBSPOT_OPTIONS_ADAPTER_PORT: {adapter_port!r}") if not nginx_conf.is_file(): raise ReadbackError(f"missing rendered nginx config: {nginx_conf}") rendered = nginx_conf.read_text() if "__BACKEND_PORT__" in rendered or "__EXP_HUBSPOT_OPTIONS_ADAPTER_PORT__" in rendered: raise ReadbackError("rendered nginx config contains unresolved port placeholders") blocks = extract_location_blocks(rendered) if active_ordinary_mode == "proxy": if ORDINARY_LOCATION in blocks: raise ReadbackError( f"ordinary proxy mode must not retain dedicated override {ORDINARY_LOCATION}" ) require_proxy_only( require_location(blocks, GENERIC_WEBHOOK_LOCATION), GENERIC_WEBHOOK_LOCATION, backend_port, ) else: expected_status = 204 if active_ordinary_mode == "ack_drop" else 403 require_return_only( require_location(blocks, ORDINARY_LOCATION), ORDINARY_LOCATION, expected_status, ) workflow_block = require_location(blocks, WORKFLOW_EXECUTE_LOCATION) if active_workflow_mode == "proxy": require_proxy_only(workflow_block, WORKFLOW_EXECUTE_LOCATION, adapter_port) else: require_return_only(workflow_block, WORKFLOW_EXECUTE_LOCATION, 403) return { "status": "PASS", "runtime_env_file": str(runtime_env_file.resolve()), "nginx_conf": str(nginx_conf.resolve()), "ordinary_mode": active_ordinary_mode, "workflow_mode": active_workflow_mode, "workflow_action_id": active_workflow_action_id, "workflow_delivery_policy": "must_complete", "workflow_callback_token_configured": True, "workflow_queue_db": active_queue_db, "workflow_replay_rps": active_replay_rps, "ack_drop_endpoints": active_ack_drop_endpoints, "ack_drop_timeout_ms": active_ack_drop_timeout_ms, "backend_port": backend_port, "workflow_adapter_port": adapter_port, } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Fail-closed readback of active prod HubSpot ingress modes" ) parser.add_argument("--runtime-env-file", required=True, type=Path) parser.add_argument("--nginx-conf", required=True, type=Path) parser.add_argument("--expected-ordinary-mode", required=True) parser.add_argument("--expected-workflow-mode", required=True) parser.add_argument("--expected-workflow-action-id", required=True) return parser.parse_args() def main() -> int: args = parse_args() try: result = verify_readback( args.runtime_env_file, args.nginx_conf, expected_ordinary_mode=args.expected_ordinary_mode, expected_workflow_mode=args.expected_workflow_mode, expected_workflow_action_id=args.expected_workflow_action_id, ) except ReadbackError as exc: raise SystemExit(f"HubSpot prod ingress mode readback failed: {exc}") from exc print(json.dumps(result, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())