Files
computer-playbook/cli/administration/inventory/validate/applications.py
T
kevinveenbirkenbachandClaude Opus 4.7 5a8fe64cc7 fix(inventory/validate): accept variants.yml keys + split CLI by concern
The inventory validator at cli/administration/inventory/validate
walked `recursive_keys(host_vars) ⊆ recursive_keys(application_defaults)`
and flagged any extra key with "Missing default for <app>: <key>".
After commit 10cb02461 ("bake only variants.yml overrides into
matrix-deploy --vars") host_vars carry every key the active
variants.yml overlay declares. Since 29377e1bd
(`_build_application_defaults` reads only `meta/<topic>.yml`, not
variants.yml — to keep the variant-0 service-key leak closed), the
defaults' allow-set is artificially narrower than the role contract:
roles/web-opt-rdr-www declares `services.dashboard.{enabled,shared}`
exclusively in `meta/variants.yml`, so the validator rejects the
baked host_vars with three "Missing default" entries before
ansible-playbook even starts (job 77797115291 on run 26428083128,
distro=debian; distro-agnostic per the matrix).

Widen the validator's legal-key set per app to

    recursive_keys(defaults[app]) ∪ ⋃ recursive_keys(variants[app][i])

variants.yml IS part of the role contract; the validator's previous
"defaults only" view was incomplete. Runtime
`_build_application_defaults` stays narrow (so 29377e1bd's
variant-0-leak fix stays in place) — only the validator widens. Typos
not declared by any variant remain caught: a new
`test_key_typo_in_variants_still_rejected` unit test pins that.

Package split (KISS/DRY/SPOT), mirroring the credentials/ refactor:

`validate/__main__.py` carried six orthogonal concerns plus
orchestration in 175 LoC. Split:

- `validate/keys.py`: `recursive_keys` (pure helper).
- `validate/loaders.py`: `load_yaml_file` (with vault-block stripping)
  and `load_inventory_files` (directory scan).
- `validate/applications.py`: `compare_application_keys` (the widened
  variants-aware logic).
- `validate/users.py`: `compare_user_keys`.
- `validate/hosts.py`: `validate_host_keys`.
- `validate/__main__.py`: thin orchestrator — argparse + load defaults/
  variants/users + invoke compare_* + exit code. 75 LoC.

`validate/__init__.py` re-exports the public API
(`main`, `compare_application_keys`, `compare_user_keys`,
`load_inventory_files`, `load_yaml_file`, `recursive_keys`,
`validate_host_keys`, `PROJECT_ROOT`). External callers
(`tests/unit/cli/administration/inventory/validate/test_main.py`,
`tests/unit/cli/administration/inventory/validate/test_hosts.py`)
keep working unchanged via this re-export.

`__main__.py` uses ABSOLUTE imports (`from cli.administration.inventory.
validate.applications import ...`) instead of relative because it's
invoked as a subprocess by tests and by the deploy entrypoint; the
sys.path bootstrap at the top of the file makes `cli.…` resolvable
in that mode.

Test layout follows the split — one test file per module:

- `test_keys.py` (6 tests: empty/non-dict/flat/nested/deep/prefix).
- `test_loaders.py` (8 tests: vault stripping, missing file, broken
  YAML, top-level + recursive directory scan, empty-block skip).
- `test_applications.py` (9 tests: all-keys-match, unknown app, extra
  key, credentials skip, variants legal, variants union, no-variants
  fallback, source path in error, typo still rejected).
- `test_users.py` (5 tests: match, unknown user warn, missing default,
  password skip, credentials skip).
- `test_hosts.py` (renamed from test_inventory_hosts.py — unit tests
  for `validate_host_keys`).
- `test_main.py` (renamed from test_inventory.py — end-to-end
  subprocess tests for `main()`, including the
  `test_variants_only_key_is_accepted` regression guard for this fix).

Coverage: 36 tests, all green. `make test` green across all 5 targets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:25:14 +02:00

30 lines
1.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Application-block validation: host_vars keys vs role contract."""
from __future__ import annotations
from .keys import recursive_keys
def compare_application_keys(applications, application_defaults, source, variants=None):
"""Variants.yml is part of the role contract: a variant overlay may
legitimately introduce service-keys absent from meta/services.yml
(e.g. roles/web-opt-rdr-www declares services.dashboard only in
meta/variants.yml). Widen the allow-set to defaults ∪ union(variant
overlays) so host_vars baked by matrix-deploy from variants.yml
aren't flagged as "Missing default"."""
variants = variants or {}
errs: list[str] = []
for app_id, conf in applications.items():
if app_id not in application_defaults:
errs.append(f"{source}: Unknown application '{app_id}'")
continue
legal_keys = recursive_keys(application_defaults[app_id])
for variant in variants.get(app_id, []):
legal_keys |= recursive_keys(variant)
for key in recursive_keys(conf):
if key.startswith("credentials"):
continue
if key not in legal_keys:
errs.append(f"{source}: Missing default for {app_id}: {key}")
return errs