mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Since 576fad84f (2026-05-10) moved role images from
`defaults/main.yml` into `meta/services.yml`, matrix-deploy's variant
baker has been writing the FULL effective config (services.yml +
variants.yml deep-merged via `get_variants`) into host_vars through
the `--vars` JSON consumed by `apply_vars_overrides`. That populates
`applications.{role}.services.{svc}.image` (and `.version`, .cpus,
etc.) with the upstream value, which then silently disables
`apply_mirror_overrides` for every role with an explicit image
declaration: the default `if_missing` policy short-circuits when the
field is already non-blank, so the rendered compose / Dockerfile FROM
keeps pulling from the upstream registry rather than the GHCR mirror.
The regression went unnoticed because Docker Hub usually serves the
pull, but it eventually surfaced as a BuildKit 429 on
`docker.io/library/php:8.2-apache` during the suitecrm Dockerfile
build (job 77602110531 on run 26362676803). Nextcloud, moodle,
mariadb consumers, postgres consumers, simpleicons, phpmyadmin,
phpldapadmin, jenkins, baserow, fider, ... all share the same latent
defect — only suitecrm happened to hit the rate limit first.
Fix the root cause instead of papering over per-role with
`mirror_policy: force`: introduce `get_variant_overrides_only` next
to `get_variants` and switch payload.py to it. The new function
returns the raw `meta/variants.yml` entries per role — no merge with
`meta/services.yml`. matrix-deploy now bakes only the variant-
specific overrides into host_vars; the role's services.yml fields
flow through the normal Ansible lookup chain at deploy time, leaving
`image`/`version` blank in host_vars so `apply_mirror_overrides`
populates them from `mirrors.yml` under its default `if_missing`
policy.
Behavior preserved for the other `get_variants` consumers:
* utils/manager/inventory.py:104 (schema-driven credentials) keeps
the full merged variant payload.
* planner.py:58 (variant-count only) is unaffected.
* legacy_resolver.py:42 (CombinedResolver runtime services_overrides)
keeps the full merged view.
Coverage:
* 3 new tests in test_applications.py for the new helper (single
empty variant when no meta/variants.yml, raw override without base
merge, cache invariance).
* test_payload.py + test_builder.py mocks repointed from `get_variants`
to `get_variant_overrides_only`.
* `make test` green across all 5 targets (lint, unit, integration,
external, test-lint) — 188 tests total.
Side-effects assessed:
* Repo-wide grep confirms no template / task accesses
`applications.X.services.Y.image` directly via hostvars — every
consumer goes through `lookup('config', …)` which still deep-merges.
* Roles previously pulling upstream now resolve via the GHCR mirror
whenever the image is already synced (the `images-mirror-missing.yml`
workflow keeps the destination populated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
2.9 KiB
Python
78 lines
2.9 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
|
|
|
|
|
|
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.
|
|
"""
|
|
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]
|
|
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
|