Skip to content

feat(converge): spec.manage describe-only protects adopted members (#325) - #327

Merged
ChrisonSimtian merged 1 commit into
mainfrom
feat/describe-only-lifecycle
Jul 28, 2026
Merged

feat(converge): spec.manage describe-only protects adopted members (#325)#327
ChrisonSimtian merged 1 commit into
mainfrom
feat/describe-only-lifecycle

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

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.

spec:
  manage: describe-only     # converge may READ this member; never write it
Verb Before After
plan DRIFT / SetConfig, every run DESCRIBE-ONLY (adopted; documented, never written), own summary figure
apply wrote to it SKIPPED, counted as skipped
destroy would destroy it REFUSED

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 exactly what this set out to stop.

Why this and not just --only

stacks/SmartHome/CLAUDE.md has always said VM 2000 is "never re-provisioned by converge --apply". That guarantee lived only in prose — nothing in the engine enforced it. #324 made it
avoidable, 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:

VM members (1) — via ProxmoxSharp:
▸ homeassistant  (vmid 2000, node hpe-01) — SetConfig

now reads:

▸ homeassistant  (vmid 2000, node hpe-01) — DESCRIBE-ONLY (adopted; documented, never written)

and --only homeassistant --apply0 applied, 0 no-change, 1 skipped, 0 failed.

Two honest notes

Destroy was never a risk for VM 2000. converge --destroy is CT-scoped and doesn't touch VM
members at all, so I found the REFUSED line didn't appear for it and corrected the shape comment
rather 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 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. Merging this refreshes the moving
schema-v1 release channel, which the stack repos validate against.

Tests

11 new (185 total). Marker parsing incl. case-insensitivity and the absent/managed defaults;
apply skips it and still handles its neighbour; --only can'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. StateDiffer only compares a field the shape actually claims.

Merge order

This first (it refreshes schema-v1), then Homelab.Stacks.SmartHome#8 which applies the marker
to VM 2000, then a pointer bump. The submodule's validate will fail against the currently pinned
schema 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 the
podman host now (podman-host/quadlets/leapmotor-mate.container). Its shape is dead weight pending
the 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.

)

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
ChrisonSimtian merged commit 00d9d0b into main Jul 28, 2026
6 checks passed
@ChrisonSimtian
ChrisonSimtian deleted the feat/describe-only-lifecycle branch July 28, 2026 11:07
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.
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.

Shapes need manage: describe-only — adopted members are protected by prose, not by the engine

1 participant