#!/usr/bin/env python3 """Validate assets.yml and expose its metadata to CI jobs.""" import argparse import json from pathlib import Path from typing import Any import yaml REQUIRED_PROJECT_FIELDS = ("id", "name", "environment") def load_inventory(path: Path) -> dict[str, Any]: with path.open(encoding="utf-8") as inventory_file: inventory = yaml.safe_load(inventory_file) if not isinstance(inventory, dict) or not isinstance(inventory.get("all"), dict): raise ValueError("inventory must contain an 'all' mapping") project = inventory["all"].get("vars", {}).get("test_project") if not isinstance(project, dict): raise ValueError("all.vars.test_project must be a mapping") missing = [field for field in REQUIRED_PROJECT_FIELDS if not project.get(field)] if missing: raise ValueError(f"test_project is missing required values: {', '.join(missing)}") children = inventory["all"].get("children", {}) if not isinstance(children, dict): raise ValueError("all.children must be a mapping") return inventory def asset_matrix(inventory: dict[str, Any]) -> list[dict[str, Any]]: assets = [] seen = set() for group_name, group in inventory["all"].get("children", {}).items(): if not isinstance(group, dict): raise ValueError(f"group '{group_name}' must be a mapping") hosts = group.get("hosts", {}) if not isinstance(hosts, dict): raise ValueError(f"group '{group_name}'.hosts must be a mapping") for asset_id, host_vars in hosts.items(): if asset_id in seen: raise ValueError(f"asset '{asset_id}' is declared more than once") seen.add(asset_id) variables = host_vars or {} if not isinstance(variables, dict): raise ValueError(f"asset '{asset_id}' variables must be a mapping") assets.append( { "id": asset_id, "group": group_name, "address": variables.get("ansible_host", variables.get("target_url", asset_id)), "asset_type": variables.get("asset_type", group_name.rstrip("s")), "test_profiles": variables.get("test_profiles", []), } ) return assets def dotenv_value(value: Any) -> str: return str(value).replace("\n", " ").replace("\r", " ") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("inventory", type=Path) parser.add_argument("--expected-environment") parser.add_argument("--dotenv", type=Path, required=True) parser.add_argument("--matrix", type=Path, required=True) args = parser.parse_args() try: inventory = load_inventory(args.inventory) assets = asset_matrix(inventory) except (OSError, ValueError, yaml.YAMLError) as error: parser.error(str(error)) project = inventory["all"]["vars"]["test_project"] if args.expected_environment and project["environment"] != args.expected_environment: parser.error( "assets.yml environment " f"'{project['environment']}' does not match TARGET_ENVIRONMENT " f"'{args.expected_environment}'" ) groups = sorted({asset["group"] for asset in assets}) dotenv = { "TEST_PROJECT_ID": project["id"], "TEST_PROJECT_NAME": project["name"], "TEST_ENVIRONMENT": project["environment"], "TEST_CUSTOMER": project.get("customer", ""), "TEST_LOCATION": project.get("location", ""), "ASSET_COUNT": len(assets), "ASSET_GROUPS": ",".join(groups), } args.dotenv.parent.mkdir(parents=True, exist_ok=True) args.matrix.parent.mkdir(parents=True, exist_ok=True) args.dotenv.write_text( "".join(f"{key}={dotenv_value(value)}\n" for key, value in dotenv.items()), encoding="utf-8", ) args.matrix.write_text(json.dumps({"assets": assets}, indent=2) + "\n", encoding="utf-8") print(f"Validated {len(assets)} assets for project '{project['name']}'") return 0 if __name__ == "__main__": raise SystemExit(main())