|
| 1 | +import argparse |
| 2 | +import json |
| 3 | +import os |
| 4 | +import sys |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +from api import login_or_fail |
| 8 | +from channeltests import ( |
| 9 | + ChannelFixture, |
| 10 | + cleanup_channel_tests, |
| 11 | + deploy_channel_tests, |
| 12 | + discover_channels, |
| 13 | + resolve_tests_root, |
| 14 | +) |
| 15 | +from compose import compose_down, compose_up, sanitize_project_name |
| 16 | + |
| 17 | +DEFAULT_TIMEOUT_SECONDS = 600 |
| 18 | +DEFAULT_BASE_URL = "https://host.docker.internal:8443" |
| 19 | +DEFAULT_USERNAME = "admin" |
| 20 | +DEFAULT_PASSWORD = "admin" |
| 21 | +DEFAULT_TESTS_ROOT = "ci/tests" |
| 22 | + |
| 23 | + |
| 24 | +def parse_args() -> argparse.Namespace: |
| 25 | + parser = argparse.ArgumentParser(description="Boot and tear down an OIE docker-compose test configuration.") |
| 26 | + parser.add_argument("--workspace", default="/workspace", help="Workspace root containing ci/configurations.") |
| 27 | + parser.add_argument("--configuration", help="Configuration name mapped to ci/configurations/<name>.compose.yml.") |
| 28 | + parser.add_argument("--compose-file", help="Explicit compose file path. Overrides --configuration.") |
| 29 | + parser.add_argument("--server-image", required=True, help="Server image tag to inject into the compose environment.") |
| 30 | + parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL used for readiness and login checks.") |
| 31 | + parser.add_argument("--username", default=DEFAULT_USERNAME, help="REST username.") |
| 32 | + parser.add_argument("--password", default=DEFAULT_PASSWORD, help="REST password.") |
| 33 | + parser.add_argument("--tests-root", default=DEFAULT_TESTS_ROOT, help="Root directory containing test fixtures.") |
| 34 | + parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SECONDS, help="Timeout in seconds for compose wait and login readiness.") |
| 35 | + parser.add_argument("--keep-alive", action="store_true", help="Leave the compose stack running after a successful check.") |
| 36 | + return parser.parse_args() |
| 37 | + |
| 38 | + |
| 39 | +def resolve_compose_file(args: argparse.Namespace) -> Path: |
| 40 | + if args.compose_file: |
| 41 | + compose_file = Path(args.compose_file) |
| 42 | + if not compose_file.is_absolute(): |
| 43 | + compose_file = Path(args.workspace) / compose_file |
| 44 | + return compose_file |
| 45 | + |
| 46 | + if not args.configuration: |
| 47 | + raise ValueError("Either --configuration or --compose-file must be provided.") |
| 48 | + |
| 49 | + return Path(args.workspace) / "ci" / "configurations" / f"{args.configuration}.compose.yml" |
| 50 | + |
| 51 | + |
| 52 | +def build_run_summary( |
| 53 | + compose_file: Path, |
| 54 | + config_name: str, |
| 55 | + project_name: str, |
| 56 | + server_image: str, |
| 57 | + tests_root: Path, |
| 58 | + channel_fixtures: list[ChannelFixture], |
| 59 | + base_url: str, |
| 60 | + keep_alive: bool, |
| 61 | +) -> dict[str, object]: |
| 62 | + return { |
| 63 | + "compose_file": str(compose_file), |
| 64 | + "configuration": config_name, |
| 65 | + "project_name": project_name, |
| 66 | + "server_image": server_image, |
| 67 | + "tests_root": str(tests_root), |
| 68 | + "channels": [str(fixture.channel_file.relative_to(tests_root)) for fixture in channel_fixtures], |
| 69 | + "base_url": base_url, |
| 70 | + "keep_alive": keep_alive, |
| 71 | + } |
| 72 | + |
| 73 | + |
| 74 | +def main() -> int: |
| 75 | + args = parse_args() |
| 76 | + compose_file = resolve_compose_file(args) |
| 77 | + tests_root = resolve_tests_root(args.workspace, args.tests_root) |
| 78 | + if not compose_file.exists(): |
| 79 | + raise FileNotFoundError(f"Compose file not found: {compose_file}") |
| 80 | + |
| 81 | + config_name = args.configuration or compose_file.stem.replace(".compose", "") |
| 82 | + channel_fixtures = discover_channels(tests_root, config_name) |
| 83 | + project_name = sanitize_project_name(config_name) |
| 84 | + env = os.environ.copy() |
| 85 | + env["OIE_IMAGE"] = args.server_image |
| 86 | + |
| 87 | + print( |
| 88 | + json.dumps( |
| 89 | + build_run_summary( |
| 90 | + compose_file, |
| 91 | + config_name, |
| 92 | + project_name, |
| 93 | + args.server_image, |
| 94 | + tests_root, |
| 95 | + channel_fixtures, |
| 96 | + args.base_url, |
| 97 | + args.keep_alive, |
| 98 | + ), |
| 99 | + indent=2, |
| 100 | + ), |
| 101 | + flush=True, |
| 102 | + ) |
| 103 | + |
| 104 | + compose_attempted = False |
| 105 | + client = None |
| 106 | + provisioned_channels = [] |
| 107 | + teardown_error = None |
| 108 | + try: |
| 109 | + compose_attempted = True |
| 110 | + compose_up(compose_file, project_name, env, args.timeout) |
| 111 | + client = login_or_fail(args.base_url, args.username, args.password, args.timeout) |
| 112 | + provisioned_channels = deploy_channel_tests(client, channel_fixtures) |
| 113 | + |
| 114 | + print(f"Configuration boot completed. Deployed {len(provisioned_channels)} channel(s).", flush=True) |
| 115 | + return 0 |
| 116 | + finally: |
| 117 | + if client is not None: |
| 118 | + cleanup_channel_tests(client, provisioned_channels) |
| 119 | + |
| 120 | + if compose_attempted and not args.keep_alive: |
| 121 | + try: |
| 122 | + compose_down(compose_file, project_name, env) |
| 123 | + except Exception as error: |
| 124 | + print(f"Teardown failed: {error}", file=sys.stderr, flush=True) |
| 125 | + teardown_error = error |
| 126 | + |
| 127 | + if teardown_error is not None: |
| 128 | + raise RuntimeError("Compose teardown failed") from teardown_error |
0 commit comments