114 lines
4.1 KiB
Python
114 lines
4.1 KiB
Python
#!/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()) |