Files
computer-playbook/cli/administration/deploy/development/inventory/payload.py
T
kevinveenbirkenbachandClaude Opus 4.7 f102c229f0 refactor(inventory): split monolith into SRP submodules + variant-only mode
Two changes ship together because the variant-only matrix-deploy logic
was written into the legacy 697-line inventory.py and then re-split:

* Source package (max 240 lines per submodule, well under the 300 budget):
  - spec.py            DevInventorySpec + PlanEntry data types
  - payload.py         _resolve_variant_payloads + _bake_overrides
  - legacy_resolver.py CombinedResolver fallback (default-mode path)
  - variants.py        strict variant-only include resolution
  - planner.py         plan_dev_inventory_matrix + filter_plan_to_variant
  - builder.py         build_dev_inventory + build_dev_inventory_matrix
  - __init__.py        public-API re-exports + back-compat shims

* Test package mirrors the source 1:1 with a shared _fixtures.make_spec
  to avoid duplicating the DevInventorySpec defaults across files.

* New variant-only matrix mode (gated on any primary shipping
  meta/variants.yml): only services whose merged variant payload pins
  both enabled and shared to literal True become inventory groups;
  Jinja flags never qualify (group_names does not exist yet at build
  time); run_after only orders the include, never expands it;
  cross-primary disagreement on a (service-key, round) raises ValueError.
  Default mode (no variants.yml on any primary) keeps the legacy
  CombinedResolver path untouched.

* Docs in design/{inventory,variants}.md updated to point at the new
  per-submodule paths; test_nginx_vhosts.py's Test classes now also
  inherit directly from unittest.TestCase so the AST-level lint in
  test_all_test_files_have_tests.py picks them up (transitive base via
  NginxVhostsTestBase alone is not enough).

Coverage: 458 cli unit tests, 349 utils unit tests, 99 dev-deploy tests,
all green; ruff check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00

73 lines
2.7 KiB
Python

"""Variant payload baking for the development inventory.
Two helpers, both pure: `_resolve_variant_payloads` picks the variant
payload for each app in a round's include set, and `_bake_overrides`
merges those payloads under `applications:` inside the `--vars` JSON
the inventory provision step consumes. Caller-supplied `extra_vars`
always win over the variant bake — that's the documented "user vars
always win" rule.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from typing import Any
from utils.cache.applications import get_variants
def _resolve_variant_payloads(
*,
roles_dir: str,
include: Sequence[str],
active_variants: Mapping[str, int],
) -> dict[str, Any]:
"""Return ``{app_id: variant_payload}`` for the requested round.
Apps without `meta/variants.yml` collapse to a single empty variant
in the loader, so this just picks variant 0 (= `meta/services.yml`
unchanged) for them. Out-of-range indices clamp to 0.
"""
variants_per_app = get_variants(roles_dir=roles_dir)
resolved: dict[str, Any] = {}
for app_id in include:
variant_list = variants_per_app.get(app_id) or [{}]
if not variant_list:
continue
index = active_variants.get(app_id, 0)
if not 0 <= index < len(variant_list):
index = 0
resolved[app_id] = variant_list[index]
return resolved
def _bake_overrides(
*,
base_overrides: Mapping[str, Any],
variant_payloads: Mapping[str, Any],
) -> dict[str, Any]:
"""Merge the per-app variant payloads under `applications:` into the
`--vars` JSON the init step will hand to
`infinito administration inventory provision`.
Caller-supplied `extra_vars` (already inside `base_overrides`) win
over the variant bake when the same key is set on both sides — that
matches the existing "user vars always win" rule."""
merged: dict[str, Any] = dict(base_overrides)
if not variant_payloads:
return merged
existing_apps = merged.get("applications")
if not isinstance(existing_apps, Mapping):
existing_apps = {}
apps: dict[str, Any] = dict(variant_payloads.items())
# Caller-supplied `applications.*` entries deep-overlay the variant
# payload so overrides like `applications.web-app-foo.feature_flag` from
# `--vars` still take precedence.
for app_id, override in existing_apps.items():
base_payload = apps.get(app_id)
if isinstance(base_payload, Mapping) and isinstance(override, Mapping):
apps[app_id] = {**base_payload, **override}
else:
apps[app_id] = override
merged["applications"] = apps
return merged