feat(converge): spec.manage describe-only protects adopted members (#325) - #327
Merged
Conversation
) An adopted guest predates the shape that describes it, so its live config will never fully match. Converge could not tell that difference was intentional, so it reported the member as drift on EVERY run and every apply wanted to "fix" it. On SmartHome that meant an unscoped plan proposing SetConfig on VM 2000 — a VM the stack's own CLAUDE.md documents as "never re-provisioned by converge --apply". That guarantee lived only in prose. #306's --only made it avoidable, but opt-in safety on a destructive default is the wrong shape for something whose failure mode is rewriting an irreplaceable HAOS install. This moves the constraint into the shape, where it cannot be forgotten. spec: manage: describe-only # converge may READ this member; never write it plan → DESCRIBE-ONLY (adopted; documented, never written), excluded from the drift count and reported as its own summary figure apply → SKIPPED, counted as skipped rather than applied destroy → REFUSED (LXC members; --destroy is CT-scoped and never touched VMs) Naming a describe-only member in --only does NOT override any of it. Allowing that would put the guarantee back in the operator's hands, which is the thing this set out to stop. DESCRIBE-ONLY IS NOT EXCLUSION. The member is still loaded, still shown in plans, and still resolvable as a dependsOn target — describing it is the point. Absent or "managed" behaves exactly as before, so no existing shape changes behaviour. Schema: `manage` added to BOTH $defs.lxcSpec and $defs.vmSpec (each has additionalProperties: false, so a shape using it would otherwise fail validation), plus the matching model properties the SchemaDriftTests tripwire requires. Verified live against SmartHome: the plan that previously ended in "homeassistant (vmid 2000) — SetConfig" now reads "DESCRIBE-ONLY (adopted; documented, never written)", and `--only homeassistant --apply` reports 1 skipped / 0 applied. 11 new tests (185 total). One of them — that drift is still reported for MANAGED members — failed on first write because the fixtures declared no comparable fields, so it was proving nothing; StateDiffer only compares a field the shape actually claims. The stack-side marker is Homelab.Stacks.SmartHome#8.
ChrisonSimtian
added a commit
that referenced
this pull request
Jul 28, 2026
…#325) Picks up Homelab.Stacks.SmartHome#8 — `spec.manage: describe-only` on the adopted HA VM 2000, now that #327 has landed the engine/schema support and refreshed the moving schema-v1 channel. With the pointer bumped, `converge stacks/SmartHome` no longer proposes a SetConfig against an adopted HAOS install: the VM reports DESCRIBE-ONLY and an apply that names it explicitly reports SKIPPED.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #325.
An adopted guest predates the shape describing it, so its live config will never fully match.
Converge couldn't tell that difference was intentional, so it reported the member as drift on
every run and every apply wanted to "fix" it.
DRIFT/SetConfig, every runDESCRIBE-ONLY (adopted; documented, never written), own summary figureSKIPPED, counted as skippedREFUSEDNaming a describe-only member in
--onlydoes not override any of it. Allowing that would putthe guarantee back in the operator's hands, which is exactly what this set out to stop.
Why this and not just
--onlystacks/SmartHome/CLAUDE.mdhas always said VM 2000 is "never re-provisioned byconverge --apply". That guarantee lived only in prose — nothing in the engine enforced it. #324 made itavoidable, but opt-in safety on a destructive default is the wrong shape for something whose failure
mode is rewriting an irreplaceable hand-built HAOS install.
Verified live
The SmartHome plan that previously ended:
now reads:
and
--only homeassistant --apply→0 applied, 0 no-change, 1 skipped, 0 failed.Two honest notes
Destroy was never a risk for VM 2000.
converge --destroyis CT-scoped and doesn't touch VMmembers at all, so I found the
REFUSEDline didn't appear for it and corrected the shape commentrather than leaving it implying a protection that wasn't in play. The marker does make destroy
refuse a describe-only LXC — that path is unit-tested.
Describe-only is not exclusion. The member is still loaded, still shown in plans, and still
resolvable as a
dependsOntarget — describing it is the point. Absent ormanagedbehaves exactlyas before, so no existing shape changes behaviour.
Schema
manageadded to both$defs.lxcSpecand$defs.vmSpec— each hasadditionalProperties: false, so a shape using it would otherwise fail validation — plus thematching model properties the
SchemaDriftTeststripwire requires. Merging this refreshes the movingschema-v1release channel, which the stack repos validate against.Tests
11 new (185 total). Marker parsing incl. case-insensitivity and the absent/
manageddefaults;apply skips it and still handles its neighbour;
--onlycan't override it; destroy refuses even with--yes; plan labels it and keeps it out of the drift count.One test — drift is still reported for managed members, guarding against the marker disabling
the drift machinery wholesale — failed on first write, because my fixtures declared no comparable
fields and so were proving nothing.
StateDifferonly compares a field the shape actually claims.Merge order
This first (it refreshes
schema-v1), then Homelab.Stacks.SmartHome#8 which applies the markerto VM 2000, then a pointer bump. The submodule's
validatewill fail against the currently pinnedschema until this lands.
Not applied to CT 4100
It looks like the other adopted member, but it's stopped and tagged
retired— Mate runs on thepodman host now (
podman-host/quadlets/leapmotor-mate.container). Its shape is dead weight pendingthe CT's deletion, not an adopted member to protect, and marking it describe-only would assert
something false. It stays a (legitimate) drift entry, noted in the stack's CLAUDE.md.