Skip to content

fix(config): normalize Packages fields when preset=lean (#134)#135

Merged
SupremeCommanderHedgehog merged 1 commit into
mainfrom
fix-134-packages-lean-normalize
Jun 20, 2026
Merged

fix(config): normalize Packages fields when preset=lean (#134)#135
SupremeCommanderHedgehog merged 1 commit into
mainfrom
fix-134-packages-lean-normalize

Conversation

@SupremeCommanderHedgehog

Copy link
Copy Markdown
Owner

Summary

Closes #134. Pre-fix, host.yaml dumped the raw default base_groups (with @standard) and required (without LEAN_EXTRA_PACKAGES) even when packages.preset: lean, while the generated ks.cfg's %packages block correctly used the lean-stripped effective set. host.yaml was lying about what was on the box — bad for the audit story since host.yaml is the source of truth consumed by ks-gen verify and audited by operators.

Fix

model_validator(mode="before") on Packages normalizes base_groups + required into the stored fields when preset == LEAN. The in-memory model now matches the install state; model_dump() (used by writer.py to produce host.yaml) becomes accurate. The effective_base_groups / effective_required properties keep their old contract for backward compat — after normalization they just echo the fields.

Why mode="before" not mode="after"

StrictModel is frozen project-wide. Pydantic v2 forbids mode="after" validators from returning a different instance on a frozen model — that's the "A custom validator is returning a value other than self" warning that surfaced when I first tried model_copy(update=...). Mutating the input dict before construction is the supported path.

Default-list extraction

Extracted the default base_groups + required lists to module-level constants (_PACKAGES_DEFAULT_BASE_GROUPS, _PACKAGES_DEFAULT_REQUIRED) so the validator can reference them when the input dict omits the field (Field(default_factory=...) values aren't applied at mode="before" time). The field default_factory lambdas reuse the constants — single source of truth.

Idempotency

Re-validating an already-normalized cfg is a no-op. Pinned by test_lean_preset_normalization_is_idempotent_on_roundtripPackages(**Packages(preset="lean").model_dump()) produces an equal cfg.

End-to-end verification

The lean golden snapshots (test_lean_preset, test_container_host_lean) include ks.cfg + tailoring + exceptions but NOT host.yaml, so the bug was invisible to the existing test suite — that's why it took an operator inspecting build/unifi/host.yaml to surface it. The fix verification is end-to-end: regenerating build/unifi/ (which uses preset: lean) post-fix produces:

packages:
  preset: lean
  base_groups:
  - '@^minimal-environment'   # ← @standard correctly absent
  required:
  - scap-security-guide
  - openscap-scanner
  - aide
  - audit
  - rsyslog
  - chrony
  - firewalld
  - sudo
  - policycoreutils-python-utils
  - dnf-automatic
  - dnf-utils
  - logrotate                  # ← LEAN_EXTRA_PACKAGES merged
  - postfix
  - cronie
  - crontabs
  - parted

Test plan

  • 7 new tests in tests/test_config_schema.py cover:
  • All existing effective_* tests (8) still pass — the properties' contract is unchanged (after normalization they echo the field values; on STANDARD they compute as before).
  • 0 golden snapshot changes — the lean fixtures don't snapshot host.yaml. Worth a future-followup test that snapshots host.yaml for a lean cfg to catch this class of bug.
  • Full CI chain run locally: ruff check && ruff format --check && mypy && pytest -q — all four green (963 passed = 956 from end of v0.29.0 + 7 new).
  • Each commit on this branch is GPG-signed with BE707B220C995478.

Related: #134 (the issue this closes).

Pre-fix, host.yaml dumped the raw default base_groups (with @standard)
and required (without LEAN_EXTRA_PACKAGES) even when packages.preset=lean,
while the generated ks.cfg correctly used the lean-stripped effective
set. host.yaml lied about what was on the box — bad for the audit story
since host.yaml is the source of truth consumed by ks-gen verify and
audited by operators.

Fix: model_validator(mode="before") on Packages normalizes base_groups
+ required into the stored fields when preset=LEAN. The in-memory model
now matches the install state; model_dump() (used by writer.py to
produce host.yaml) becomes accurate. The effective_base_groups and
effective_required properties keep their old contract for backward
compat — after normalization they just echo the fields.

Why mode="before" not "after": StrictModel is frozen project-wide.
Pydantic v2 forbids mode="after" validators from returning a different
instance on a frozen model (via model_copy or otherwise) — that's the
"A custom validator is returning a value other than `self`" warning.
Mutating the input dict before construction is the supported path.

Also: extracted the default base_groups + required lists to module-level
constants (_PACKAGES_DEFAULT_BASE_GROUPS, _PACKAGES_DEFAULT_REQUIRED) so
the validator can reference them when the input dict omits the field
(default_factory values aren't applied at mode="before" time). The
field default_factory lambdas reuse the constants — single source of
truth.

Idempotent: re-validating an already-normalized cfg is a no-op. Pinned
by test_lean_preset_normalization_is_idempotent_on_roundtrip.

7 new tests cover: lean normalizes base_groups into field; lean
normalizes required into field; reflected in model_dump (the headline
acceptance criterion from #134); roundtrip idempotency; user-supplied
required order preserved + lean extras deduplicated; user-supplied
custom @group preserved while @standard is dropped; STANDARD preset
unchanged (no spurious normalization).

No golden snapshot changes: the existing lean snapshots
(test_lean_preset, test_container_host_lean) include ks.cfg + tailoring
+ exceptions but NOT host.yaml, so the bug was invisible to the test
suite. ubuntu_minimal does snapshot host.yaml but uses preset=standard.
The actual fix verification is end-to-end: regenerating build/unifi/
(lean preset) post-fix produces a host.yaml with no @standard in
base_groups and all 5 LEAN_EXTRA_PACKAGES in required.

Fixes #134.
@SupremeCommanderHedgehog SupremeCommanderHedgehog merged commit 84a460b into main Jun 20, 2026
6 checks passed
@SupremeCommanderHedgehog SupremeCommanderHedgehog deleted the fix-134-packages-lean-normalize branch June 20, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(config): packages.preset=lean — host.yaml dump still shows @standard in base_groups (display lies vs actual install)

1 participant