Files
computer-playbook/cli/administration/deploy/development/inventory/builder.py
T
kevinveenbirkenbachandClaude Opus 4.7 a10555cd08 feat(inventory): variant-aware credentials generation
InventoryManager.resolve_schema_includes_recursive previously read the
root role's base meta/services.yml when discovering shared providers,
regardless of which matrix variant the planner had pinned for the
round. For a role whose variants.yml enables additional services per
variant (e.g. svc-bkp-container-2-local variant 2 enabling
services.ldap), the base config had no `enabled+shared` flag for those
services, so the recursive resolver never visited the variant's
shared providers — their credentials were never generated, and the
deploy crashed in the provider's env.j2 with "credentials is defined
in schema at roles/<provider>/meta/schema.yml but not set in
application '<provider>'".

Wires the active variant through the existing inventory pipeline:

- utils/manager/inventory.py: InventoryManager(variant=None|int). When
  set, load_role_config_by_path overlays variants.yml[N] onto the root
  role's meta config (only for the root — transitive providers keep
  their own base lookup). variant=None preserves the pre-refactor base
  behavior for every standalone caller.
- cli/administration/inventory/credentials: new --variant N flag,
  forwarded to InventoryManager.
- cli/administration/inventory/provision: new --app-variants <JSON>
  flag ({app_id: variant_index}), forwarded per-app to the credentials
  subprocess.
- cli/administration/deploy/development/inventory/builder.py: emits
  --app-variants from spec.variant_selectors() when the round pins any
  variant. Apps without variants.yml collapse to a single empty
  overlay in get_variants so this is a no-op for them.

10 unit tests cover: variant overlay for root role, variant=None
fallback to base, out-of-range fallback, no overlay on non-root
paths, --variant CLI forwarding (set + default), --app-variants
generator forwarding (set + omit), builder --app-variants emission
(set + omit).

Caveat: bkp-c2l CI variant 1 still loads svc-db-openldap transitively
at runtime (outside the planner's variant closure) — a separate
sys-service-loader / role-meta bug, not addressed here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:04:20 +02:00

143 lines
4.5 KiB
Python

"""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_on_ci,
)
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_on_ci():
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["INFINITO_SERVICES_DISABLED"] = 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 INFINITO_SERVICES_DISABLED 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,
)