Files
computer-playbook/cli/administration/deploy/development/inventory/payload.py
T
kevinveenbirkenbachandClaude Opus 5 f1635e6a59 feat(variants): let a variant dictate the config of the providers it pulls in
A round deploys a provider once, so the role that pulls it in is the only place that can say how lean it has to be. A variant entry may now carry any application config topic under a service it depends on: a mapping deep-merges, so naming one key changes that key alone, and null empties the topic. Empty was the case that needed a marker of its own, because merging an empty mapping is a no-op and would leave exactly the services the round was trying to shed.

The topic vocabulary and the merge semantics live in utils.roles.applications.topics, which is also where resources.py now gets them rather than spelling the same null-or-merge out a second time. Collecting who demands what stays in the inventory tree, refuses two roles demanding different values for one provider in the same round, and skips a role's own entity entry so the existing services.<own>.users declarations are not read as overrides.

The CI set-cover learns the same fact. Its greedy cover retires a role's variant-0 row when an earlier row of another role embeds it, on the stated premise that embedding pulls the provider's variant 0. A variant that dictates that provider's config breaks the premise from the other side: whatever ran, it was not the provider as the provider declares it. Such a pair now covers nothing, so the retired row keeps its job, its artefact and its verdict.

Measured on the current tree: 20 override pairs, 41 covered rows before and after, so nothing is revoked today. The rule bites the moment an override lands in a row that covers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:08:08 +02:00

91 lines
3.4 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_variant_overrides_only
from .nested_overrides import apply_topic, collect_provider_overrides
def _resolve_variant_payloads(
*,
roles_dir: str,
include: Sequence[str],
active_variants: Mapping[str, int],
) -> dict[str, Any]:
"""Return ``{app_id: variant_override}`` for the requested round.
Each value is the raw `meta/variants.yml` entry — NOT the
deep-merge of variants.yml on top of `meta/services.yml`. Baking
only the override keeps host_vars sparse so downstream passes
(notably `apply_mirror_overrides`) can populate fields like
`image`/`version` that the role declares in `meta/services.yml`.
Apps without `meta/variants.yml` collapse to a single empty
override `{}`. Out-of-range indices clamp to 0.
A config topic a variant declares under a service it depends on is
finally moved onto that service's provider, so the role pulling a
dependency in is what decides how lean the dependency runs.
"""
overrides_per_app = get_variant_overrides_only(roles_dir=roles_dir)
resolved: dict[str, Any] = {}
for app_id in include:
variant_list = overrides_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]
for provider, topics in collect_provider_overrides(
resolved, roles_dir=roles_dir
).items():
if provider not in resolved:
continue
payload = resolved[provider]
payload = dict(payload) if isinstance(payload, Mapping) else {}
for topic, value in topics.items():
payload[topic] = apply_topic(payload.get(topic), value)
resolved[provider] = payload
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())
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