feat(reconcile): model volume recreation in the plan - #13962
Conversation
Move volume divergence detection and recreation out of the imperative pre-reconcile path (ensureVolume/removeDivergedVolume) and into the reconciliation plan, activating the dormant planRecreateVolume seam. A diverged volume now produces an explicit, forward-only sequence: stop containers -> remove containers -> remove volume -> create volume -> create containers. Container re-creation is delegated to reconcileContainers (affected services are cleared from the observed snapshot so they are scheduled fresh, gated on the CreateVolume node), and the recreation cascades to namespace/volume-sharing dependents. User confirmation (recreate, data will be lost) is consulted while building the plan via reconciler.prompt; declining leaves the volume untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The PR successfully moves volume divergence detection and recreation from the imperative pre-reconcile path (ensureVolume/removeDivergedVolume) into the reconciliation plan. The reconcileVolumes/planRecreateVolumes structure is sound: it collects diverged volumes, gates on user confirmation, stops and removes affected containers in dependency order, removes then recreates volumes, and hands container re-creation to reconcileContainers. The stoppedByPlan deduplication correctly prevents double-stops when a container is shared between network and volume recreation paths. The test suite is comprehensive.
Lower-confidence findings (not posted inline)
-
[low]
pkg/compose/reconcile.go:114—observedContainersByServicesnapshot not updated after containers cleared for volume recreation (confidence: 🟠 weak 52/100)
r.observed.Containers[svc]is set tonilat line 371 when services are cleared for volume re-creation, butr.observedContainersByService(built once at reconciler init viaobserved.containersByService()) still holds old containers for those services. WhenserviceHashWithResolvedRefsis called for avolumes_fromdependent of a recreated service, it resolves old container IDs from the stale snapshot. In practice,parentNamespaceRecreatedcatches this case and forces recreation regardless of hash, so no incorrect decision is made on the happy path — but the two data structures are now diverged, which is a latent inconsistency worth noting for future maintainers. -
[medium]
pkg/compose/reconcile.go:371—volumes_fromdependents may not have correct creation ordering after owner service volume recreation (confidence: 🟠 weak 45/100)
When service B is cleared for volume recreation (r.observed.Containers[B] = nil,r.recreatedServices[B] = true), a dependent service A withvolumes_from: [B]is NOT cleared from observed state.reconcileContainerscorrectly forces recreation of A viaparentNamespaceRecreated, but whether A's new container creation node depends (transitively) on theCreateVolume → CreateContainer(B)chain depends on whetherinfrastructureDeps(A)capturesr.serviceNodes[B]. If it does (likely given topological processing order), ordering is correct. If not, A could start before B's fresh container is ready. The verifier rated this speculative without seeinginfrastructureDepsinternals.
servicesUsingVolume only matched services mounting the volume directly, so a service reaching it through volumes_from was not stopped/removed before RemoveVolume. Docker materializes the inherited mount on the consumer's container, so its removal would fail with "volume in use". Compute the transitive volumes_from closure so every container referencing the volume is removed first. (network_mode/ipc/pid: service:x share namespaces, not mounts, and are intentionally excluded.) Also reassign the result of Labels.Add in createVolume: it mutates in place only when the map is non-nil, so discarding the return would drop the config-hash label for a volume with no CustomLabels. Addresses review feedback: documents why observed.Containers is cleared without touching the observedContainersByService hashing snapshot, and strengthens the cascade tests to assert the full plan ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
|
Thanks for the review. Adjudication of the two findings, plus a related issue they surfaced — addressed in 72f540c. [medium] [low] Real issue surfaced by this review — Also reassigned the result of |
glours
left a comment
There was a problem hiding this comment.
Two edge regressions from the switch to label-filtered observed state, both verified against the old ensureVolume path:
-
A same-named volume created manually or by another project used to be reused with just a warning (inspect by name). It's now invisible to the observed state, so we plan a
VolumeCreateon everyup: hard failure if the driver differs, spurious
Creating/Createdevents forever otherwise.checkVolumesshould probably inject those unlabeled-but-existing volumes into the observed state, like it already does for externals. -
Renaming a volume now hits the diverged path: the old volume is matched by label and
VolumeHashincludesName, so withup -ywe delete the old volume and its data, where we previously just created the new one and left the old untouched. The
prompt also names the new volume, which doesn't exist yet. Whenobserved.Name != desired.NameI'd keep the additive behavior, or at least prompt with the name of the volume we're about to delete.
Two edge regressions from the switch to a label-scoped observed state, both reported against the old ensureVolume path: - A same-named volume created manually or by another project (no compose label) was invisible to the observed state, so a VolumeCreate was planned on every up: a hard failure if the driver differed, spurious Creating/Created events otherwise. collectObservedState now discovers such volumes by name (pre-label Compose semantics) and records them as unmanaged matches with an empty config-hash, so the reconciler reuses them untouched. The ownership warnings move to warnUnmanagedVolumes, driven off the observed state; checkVolumes shrinks to external-only validation (checkExternalVolumes). - Renaming a volume hit the diverged path and, with up -y, deleted the old volume and its data (VolumeHash includes Name), where it previously just created the new one. When observed.Name != desired.Name the volume is now created additively, leaving the old one untouched, with no prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
|
Thanks @glours, both fixed in 2ba485b.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…ame up The additive rename path created the new volume but kept the old name in the observed state, so hasVolumeMismatch never fired: existing containers stayed on the old volume while fresh replicas mounted the new one (split-brain), and later runs picked a nondeterministic winner between the two equally labelled volumes. Rewrite the observed volume name to the desired one after planning the "renamed" create, so reconcileContainers migrates the existing containers onto the new volume in the same up — restoring parity with the old ensureVolume path — while still leaving the old volume and its data intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
|
Follow-up on the rename case (f16d0bc): the additive fix kept the old name in the observed state, so existing containers stayed on the old volume while fresh replicas mounted the new one (split-brain), and later runs picked a nondeterministic winner between the two equally-labelled volumes. Now the observed volume name is rewritten to the desired one after planning the |
Mirror the volume reconciliation work (docker#13962) for networks: move network divergence detection and recreation out of the imperative pre-reconcile path (ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork) into the reconciliation plan. - reconcileNetworks now owns creation of missing networks and, for a network whose config-hash diverged, an explicit recreation sequence (no user confirmation: recreating a network is not destructive): stop containers -> disconnect -> remove network -> create network -> reconnect containers. Attached containers keep their identity (they are reconnected, not recreated), matching the previous behavior. If a container is independently recreated by reconcileContainers, its removal is ordered after the reconnect so they don't race. - Renaming a network creates the new one additively and leaves the old one untouched. - collectObservedState discovers legacy/unlabeled networks by name and records them as unmanaged matches (empty config-hash) so the reconciler reuses them untouched; ownership warnings move to warnUnmanagedNetworks. checkExternalNetworks keeps external-network validation/resolution. - execCreateNetwork now issues a plain createNetwork; the imperative ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork and the connect/disconnect helpers are removed. Adds reconcile, observed-state and executor tests covering network create/diverge/rename, the entangled diverge+recreate case, legacy by-name discovery and the ownership warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Mirror the volume reconciliation work (#13962) for networks: move network divergence detection and recreation out of the imperative pre-reconcile path (ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork) into the reconciliation plan. - reconcileNetworks now owns creation of missing networks and, for a network whose config-hash diverged, an explicit recreation sequence (no user confirmation: recreating a network is not destructive): stop containers -> disconnect -> remove network -> create network -> reconnect containers. Attached containers keep their identity (they are reconnected, not recreated), matching the previous behavior. If a container is independently recreated by reconcileContainers, its removal is ordered after the reconnect so they don't race. - Renaming a network creates the new one additively and leaves the old one untouched. - collectObservedState discovers legacy/unlabeled networks by name and records them as unmanaged matches (empty config-hash) so the reconciler reuses them untouched; ownership warnings move to warnUnmanagedNetworks. checkExternalNetworks keeps external-network validation/resolution. - execCreateNetwork now issues a plain createNetwork; the imperative ensureNetwork/resolveOrCreateNetwork/removeDivergedNetwork and the connect/disconnect helpers are removed. Adds reconcile, observed-state and executor tests covering network create/diverge/rename, the entangled diverge+recreate case, legacy by-name discovery and the ownership warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
What I did
Move volume divergence detection and recreation out of the imperative
pre-reconcile path (
ensureVolume/removeDivergedVolume) into thereconciliation plan, activating the dormant
planRecreateVolumeseam.A diverged volume now produces an explicit, forward-only plan:
stop → remove containers → remove volume → create volume → create containers.Container re-creation is delegated to
reconcileContainers(affectedservices are cleared from the observed snapshot so they are scheduled
fresh, gated on the
CreateVolumenode), and the recreation cascades tonamespace/volume-sharing dependents.
User confirmation ("recreate, data will be lost") is consulted while
building the plan via
reconciler.prompt; declining leaves the volumeuntouched. Behavior otherwise matches the previous
ensureVolume(norecreation for a volume with no recorded config-hash, external volumes
validated up front).
Adds an exhaustive reconcile test suite for the volume lifecycle and its
interaction with services (single/scale-N/multi-service recreation,
decline, missing hash, prompt message/error, container mounting several
diverged volumes, partial confirmation, dependent cascade, unmounted
volume) plus an executor test driving the destructive sequence end to end.
Related issue
🤖 Generated with Claude Code