"""Inventory folder materialisation. `build_dev_inventory` is the SPOT that turns a `DevInventorySpec` into a fully baked inventory folder via the `infinito administration inventory provision` CLI. `build_dev_inventory_matrix` is the orchestrator: it runs the planner and builds every round's folder. """ from __future__ import annotations import json import os from typing import TYPE_CHECKING, Any from cli.administration.deploy.development.common import DEV_INVENTORY_VARS_FILE from cli.administration.deploy.development.mirrors import ( generate_ci_mirrors_file, should_use_mirrors, ) from .payload import _bake_overrides, _resolve_variant_payloads from .planner import plan_dev_inventory_matrix from .spec import DevInventorySpec, PlanEntry if TYPE_CHECKING: from collections.abc import Mapping, Sequence from cli.administration.deploy.development.compose import Compose def build_dev_inventory(compose: Compose, spec: DevInventorySpec) -> None: """Build the inventory folder described by `spec` and ensure its vault password file exists. Two side-effects, no return value: after this call, `spec.inventory_dir` contains a complete, variant-resolved inventory. """ inv_root = spec.inventory_root() variant_payloads = _resolve_variant_payloads( roles_dir=str(compose.repo_root / "roles"), include=spec.include, active_variants=spec.variant_selectors(), ) vars_payload = _bake_overrides( base_overrides=spec.overrides(), variant_payloads=variant_payloads, ) cmd = [ "infinito", "administration", "inventory", "provision", inv_root, "--host", "localhost", "--vars-file", DEV_INVENTORY_VARS_FILE, "--vars", json.dumps(vars_payload, sort_keys=True), "--include", ",".join(spec.include), ] variant_selectors = spec.variant_selectors() if variant_selectors: cmd += ["--app-variants", json.dumps(variant_selectors, sort_keys=True)] if should_use_mirrors(): mirrors_file = generate_ci_mirrors_file(compose, inventory_dir=inv_root) cmd += ["--mirror", mirrors_file] extra_env: dict[str, str] = {} if spec.services_disabled: extra_env["disable"] = spec.services_disabled compose.exec( cmd, check=True, workdir=os.environ["INFINITO_SRC_DIR"], extra_env=extra_env or None, ) _ensure_vault_password_file(compose, inventory_dir=inv_root) def build_dev_inventory_matrix( compose: Compose, *, base_inventory_dir: str, primary_apps: Sequence[str], storage_constrained: bool, runtime: str, extra_vars: Mapping[str, Any] | None = None, services_disabled: str = "", include_filter: Sequence[str] | None = None, ) -> list[PlanEntry]: """Build every folder in the matrix plan and return the plan. `include_filter`, when provided, is the set of role names a caller has already filtered (e.g. by `disable` removal of provider roles). Each round's include is intersected with this set before being baked, so the inventory and `--include` flag stay aligned with whatever the deploy step will actually deploy. """ plan = plan_dev_inventory_matrix( roles_dir=str(compose.repo_root / "roles"), primary_apps=primary_apps, base_inventory_dir=base_inventory_dir, ) allow: set[str] | None = set(include_filter) if include_filter is not None else None for _round_index, inv_dir, round_variants, raw_include, _purge_set in plan: round_include = ( tuple(role for role in raw_include if role in allow) if allow is not None else raw_include ) spec = DevInventorySpec( inventory_dir=inv_dir, include=round_include, storage_constrained=storage_constrained, runtime=runtime, extra_vars=extra_vars, services_disabled=services_disabled, active_variants=round_variants, ) build_dev_inventory(compose, spec) return plan def _ensure_vault_password_file(compose: Compose, *, inventory_dir: str) -> None: inv_root = str(inventory_dir).rstrip("/") pw_file = f"{inv_root}/.password" compose.exec( [ "sh", "-lc", ( f"mkdir -p {inv_root} && " f"[ -f {pw_file} ] || " f"printf '%s\n' 'ci-vault-password' > {pw_file}" ), ], check=True, )