Files
computer-playbook/cli/administration/deploy/development/inventory/planner.py
T
kevinveenbirkenbachandClaude Opus 4.7 bd1a3a69ad fix(deploy/development/inventory/planner): split per-round include from union purge_set
`3c1d5d821` solved the cross-round backbone-abriss problem by making
`include` equal the union of every variant's closure for every round.
Side-effect: every matrix round then ALSO deployed the full union, so
`08_deploy_performance.sh` saw `dashboard.infinito.example` still
answering HTTP 200 after a matomo-only `deploy-fresh-purged-apps` —
the deploy step was no longer matomo-only at all.

Split the two concerns instead:

- `include` (PlanEntry[3]) goes back to the per-round variant-closure.
  Both `init`/`builder` (`--include` for the inventory bake) and
  `deploy` (`--id` for ansible) consume it. Each round therefore
  deploys ONLY its own variant-closure again — the pre-`3c1d5d821`
  semantics.
- `purge_set` (PlanEntry[4], new) carries the union across all rounds.
  `deploy.py` uses it for the inter-round wipe instead of the previous
  round's include. Result: between rounds the WHOLE matrix footprint
  is purged, not just the previous round's variant slice — a variant
  pinning `services.<X>.shared: false` cannot leave its bundled
  provider on the host for a sibling round that wires the same service
  shared. The backbone-abriss `3c1d5d821` was hunting stays fixed:
  whichever round needs the backbone re-deploys it immediately after
  the wipe (purge runs at the start of each round, not the end).

`previous_round_include` tracking is gone. The first iteration skips
the purge (nothing to wipe yet), which also covers `total_rounds == 1`
and `--variant N` pinned single-folder modes cleanly.

Unrelated, but staged together because it is part of the same
investigation:

- `roles/dev-nix/templates/nix.conf.j2`: enable `keep-derivations` /
  `keep-outputs`, so `nix-collect-garbage -d` (run by the aggressive
  prune step) does not drop the build closure of pkgmgr's flake; this
  was making every deploy re-fetch the build inputs from
  cache.nixos.org and overshoot pkgmgr's update timeout.
- `roles/pkgmgr/tasks/01_core.yml`: raise the pkgmgr update timeout
  from 600s to 1800s and lower the retry count from 6 to 3, so a slow
  fetch on a single try no longer pushes total wallclock past three
  hours.

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

114 lines
4.3 KiB
Python

"""Matrix-deploy planner.
Pure function — reads role metadata via cached YAML helpers and emits
``PlanEntry`` tuples; no filesystem side effects. The include set per
round is resolved by the ``CombinedResolver`` (see ``.legacy_resolver``):
each role contributes its own pulled-in providers and the include is
the union. A service-key is therefore pulled in as soon as ANY role in
the round-aware closure says it should be (literal ``True`` OR the
``"{{ '<role>' in group_names }}"`` Jinja form, both accepted by
``utils.roles.applications.services.registry.is_explicit_truth``); if
EVERY role marks it ``False`` it stays out of the include.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from utils.cache.applications import get_variants
from . import legacy_resolver
if TYPE_CHECKING:
from collections.abc import Sequence
from .spec import PlanEntry
def filter_plan_to_variant(
plan: list[PlanEntry],
variant: int | None,
) -> list[PlanEntry]:
"""Pin a matrix plan to a single round when `variant` is set.
`variant=None` returns the plan unchanged (full-matrix mode). An
explicit variant index is matched against the plan's round indices;
if it's out of range, raises `ValueError` so callers can surface a
clean operator-facing error rather than silently doing nothing.
"""
if variant is None:
return plan
for entry in plan:
if entry[0] == variant:
return [entry]
available = sorted(entry[0] for entry in plan)
raise ValueError(f"variant {variant} out of range; available rounds: {available}")
def plan_dev_inventory_matrix(
*,
roles_dir: str,
primary_apps: Sequence[str],
base_inventory_dir: str,
) -> list[PlanEntry]:
"""Return ``[(round_index, inventory_dir, round_variants, include, purge_set), ...]`` — per-round variant-closure plus a plan-constant union for the inter-round wipe."""
if not primary_apps:
raise ValueError("plan_dev_inventory_matrix: primary_apps must not be empty")
variants_per_app = get_variants(roles_dir=roles_dir)
primary_variant_counts = {
app_id: max(1, len(variants_per_app.get(app_id) or [{}]))
for app_id in primary_apps
}
total_rounds = max(primary_variant_counts.values(), default=1)
base = str(base_inventory_dir).rstrip("/")
# WHY split: per-round include is variant-specific; purge_set is the union so the inter-round wipe clears every variant's footprint, not just the previous round's slice.
per_variant_includes: list[tuple[str, ...]] = []
primary_round_variants_per_round: list[dict[str, int]] = []
for round_index in range(total_rounds):
primary_round_variants = {
app_id: round_index if round_index < count else 0
for app_id, count in primary_variant_counts.items()
}
primary_round_variants_per_round.append(primary_round_variants)
services_overrides = legacy_resolver._build_services_overrides_for_round(
roles_dir=roles_dir,
round_index=round_index,
primary_app_variants=primary_round_variants,
)
per_variant_includes.append(
legacy_resolver._resolve_round_include(
primary_apps=primary_apps,
services_overrides=services_overrides,
)
)
seen_union: set[str] = set()
union_apps: list[str] = []
for variant_include in per_variant_includes:
for dep in variant_include:
if dep not in seen_union:
seen_union.add(dep)
union_apps.append(dep)
purge_set: tuple[str, ...] = tuple(union_apps)
plan: list[PlanEntry] = []
for round_index in range(total_rounds):
primary_round_variants = primary_round_variants_per_round[round_index]
round_include = per_variant_includes[round_index]
round_variants = dict(primary_round_variants)
for dep in round_include:
if dep in round_variants:
continue
dep_variants = variants_per_app.get(dep) or [{}]
dep_count = max(1, len(dep_variants))
round_variants[dep] = round_index if round_index < dep_count else 0
inv_dir = f"{base}-{round_index}" if total_rounds > 1 else base
plan.append((round_index, inv_dir, round_variants, round_include, purge_set))
return plan