1212
1313INTEGRATION_VERSION = "m6-container-integration-v1"
1414COMPOSE = ["docker" , "compose" , "--env-file" , "infra/.env" , "-f" , "infra/docker-compose.yml" ]
15+ ENV_FILE = Path ("infra/.env" )
16+ DEFAULT_API_PORT = 8000
17+
18+
19+ def _validate_port (raw_value : str , * , source : str ) -> int :
20+ try :
21+ port = int (raw_value )
22+ except ValueError as exc :
23+ raise ValueError (f"{ source } must be an integer in range 1-65535" ) from exc
24+ if not 1 <= port <= 65535 :
25+ raise ValueError (f"{ source } must be an integer in range 1-65535" )
26+ return port
27+
28+
29+ def resolve_api_port (env_file : Path = ENV_FILE , * , explicit_port : str | None = None ) -> int :
30+ if explicit_port is not None :
31+ return _validate_port (explicit_port , source = "--api-port" )
32+ if not env_file .exists ():
33+ return DEFAULT_API_PORT
34+ for raw_line in env_file .read_text (encoding = "utf-8" ).splitlines ():
35+ line = raw_line .strip ()
36+ if not line or line .startswith ("#" ) or "=" not in line :
37+ continue
38+ key , value = line .split ("=" , 1 )
39+ if key .strip () == "FABOPS_API_PORT" :
40+ return _validate_port (value .strip (), source = "FABOPS_API_PORT" )
41+ return DEFAULT_API_PORT
1542
1643
1744def _run (arguments : list [str ], * , timeout : int = 300 ) -> dict [str , Any ]:
@@ -27,18 +54,19 @@ def _run(arguments: list[str], *, timeout: int = 300) -> dict[str, Any]:
2754 }
2855
2956
30- def _read_health (timeout_seconds : float = 60.0 ) -> dict [str , Any ] | None :
57+ def _read_health (host : str , port : int , timeout_seconds : float = 60.0 ) -> dict [str , Any ] | None :
3158 deadline = time .monotonic () + timeout_seconds
59+ health_url = f"http://{ host } :{ port } /health/ready"
3260 while time .monotonic () < deadline :
3361 try :
34- with urllib .request .urlopen ("http://127.0.0.1:8000/health/ready" , timeout = 3 ) as response :
62+ with urllib .request .urlopen (health_url , timeout = 3 ) as response :
3563 return json .load (response )
3664 except (OSError , urllib .error .URLError , json .JSONDecodeError ):
3765 time .sleep (1.0 )
3866 return None
3967
4068
41- def generate (output : Path , * , keep_up : bool = False ) -> dict [str , Any ]:
69+ def generate (output : Path , * , keep_up : bool = False , api_port_override : str | None = None ) -> dict [str , Any ]:
4270 generated_at = datetime .now (timezone .utc ).isoformat ()
4371 commands : list [dict [str , Any ]] = []
4472 docker_info = _run (["docker" , "info" ], timeout = 30 )
@@ -68,6 +96,55 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
6896 output .write_text (json .dumps (result , indent = 2 , sort_keys = True ) + "\n " , encoding = "utf-8" )
6997 return result
7098
99+ if not ENV_FILE .exists ():
100+ result = {
101+ "schema_version" : INTEGRATION_VERSION ,
102+ "generated_at" : generated_at ,
103+ "status" : "unverified" ,
104+ "reason" : "Local server-only infra/.env unavailable; container-backed integration was not executed" ,
105+ "compose_config_verified" : False ,
106+ "postgres_runtime_verified" : False ,
107+ "redpanda_runtime_verified" : False ,
108+ "neo4j_runtime_verified" : False ,
109+ "container_integration_verified" : False ,
110+ "api_restart_verified" : False ,
111+ "docker_daemon_available" : True ,
112+ "api_port" : DEFAULT_API_PORT ,
113+ "commands" : [{"command" : "docker info" , "exit_code" : docker_info ["exit_code" ]}],
114+ "reproduction_commands" : [
115+ "cp infra/.env.example infra/.env" ,
116+ "chmod 0600 infra/.env" ,
117+ "docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet" ,
118+ ],
119+ }
120+ output .parent .mkdir (parents = True , exist_ok = True )
121+ output .write_text (json .dumps (result , indent = 2 , sort_keys = True ) + "\n " , encoding = "utf-8" )
122+ return result
123+
124+ try :
125+ api_port = resolve_api_port (ENV_FILE , explicit_port = api_port_override )
126+ except ValueError :
127+ result = {
128+ "schema_version" : INTEGRATION_VERSION ,
129+ "generated_at" : generated_at ,
130+ "status" : "unverified" ,
131+ "reason" : "Invalid integration API port configuration; expected an integer in range 1-65535" ,
132+ "compose_config_verified" : False ,
133+ "postgres_runtime_verified" : False ,
134+ "redpanda_runtime_verified" : False ,
135+ "neo4j_runtime_verified" : False ,
136+ "container_integration_verified" : False ,
137+ "api_restart_verified" : False ,
138+ "docker_daemon_available" : True ,
139+ "commands" : [{"command" : "docker info" , "exit_code" : docker_info ["exit_code" ]}],
140+ "reproduction_commands" : [
141+ "docker compose --env-file infra/.env -f infra/docker-compose.yml config --quiet" ,
142+ ],
143+ }
144+ output .parent .mkdir (parents = True , exist_ok = True )
145+ output .write_text (json .dumps (result , indent = 2 , sort_keys = True ) + "\n " , encoding = "utf-8" )
146+ return result
147+
71148 compose_config = _run ([* COMPOSE , "config" , "--quiet" ], timeout = 30 )
72149 commands .append ({key : value for key , value in compose_config .items () if key not in {"stdout_tail" , "stderr_tail" }})
73150 compose_config_verified = compose_config ["exit_code" ] == 0
@@ -84,6 +161,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
84161 "container_integration_verified" : False ,
85162 "api_restart_verified" : False ,
86163 "docker_daemon_available" : True ,
164+ "api_port" : api_port ,
87165 "commands" : commands ,
88166 "reproduction_commands" : [compose_config ["command" ]],
89167 }
@@ -99,7 +177,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
99177 restart : dict [str , Any ] = {"exit_code" : 1 , "command" : "not executed" , "duration_seconds" : 0.0 }
100178 try :
101179 if up ["exit_code" ] == 0 :
102- health = _read_health ()
180+ health = _read_health ("127.0.0.1" , api_port )
103181 integration_test = _run (
104182 [
105183 * COMPOSE ,
@@ -120,7 +198,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
120198 restart = _run ([* COMPOSE , "restart" , "api" ], timeout = 60 )
121199 commands .append ({key : value for key , value in restart .items () if key not in {"stdout_tail" , "stderr_tail" }})
122200 if restart ["exit_code" ] == 0 :
123- restart_health = _read_health ()
201+ restart_health = _read_health ("127.0.0.1" , api_port )
124202 finally :
125203 if not keep_up :
126204 down = _run ([* COMPOSE , "down" ], timeout = 120 )
@@ -149,6 +227,7 @@ def generate(output: Path, *, keep_up: bool = False) -> dict[str, Any]:
149227 "status" : "verified" if container_verified else "degraded" ,
150228 "reason" : None if container_verified else "one or more container-backed runtime checks failed" ,
151229 "docker_daemon_available" : True ,
230+ "api_port" : api_port ,
152231 "compose_config_verified" : compose_config_verified ,
153232 "postgres_runtime_verified" : postgres_verified ,
154233 "redpanda_runtime_verified" : redpanda_verified ,
@@ -181,9 +260,10 @@ def main() -> None:
181260 parser = argparse .ArgumentParser (description = "Verify the isolated M6 Docker Compose integration stack." )
182261 parser .add_argument ("--output" , type = Path , default = Path ("evidence/m6/integration-summary.json" ))
183262 parser .add_argument ("--keep-up" , action = "store_true" )
263+ parser .add_argument ("--api-port" , help = "Override the loopback API port used for readiness probes." )
184264 parser .add_argument ("--check" , action = "store_true" )
185265 args = parser .parse_args ()
186- result = generate (args .output , keep_up = args .keep_up )
266+ result = generate (args .output , keep_up = args .keep_up , api_port_override = args . api_port )
187267 if args .check and result ["docker_daemon_available" ] and not result ["container_integration_verified" ]:
188268 raise SystemExit ("M6 container integration verification failed" )
189269
0 commit comments