Skip to content

fix: provider migration condemns the service's stale replicas - #14264

Merged
ndeloof merged 1 commit into
docker:mainfrom
ndeloof:provider-migration-cleanup
Sep 25, 2026
Merged

ndeloof merged 1 commit into
docker:mainfrom
ndeloof:provider-migration-cleanup

Conversation

@ndeloof

@ndeloof ndeloof commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

What this PR does, in one sentence: migrating an already-deployed service to a provider now works — the first up after the migration replaces the service's old containers with the provider's relay instead of failing on a name conflict.

Context

Take a deployed compose application and migrate one of its services to a provider (provider: type: …). On the next up, the old service container is in a blind spot: the reconciler deliberately leaves provider services' containers alone (that rule protects the relay), and the container is not an orphan either — its service is still in the model. Nobody removes it. The provider then runs, publishes its endpoints, and compose fails deploying the relay:

Relay myapp-api-1 Creating
provider:api create relay for service api: Error response from daemon: Conflict.
The container name "/myapp-api-1" is already in use by container "729d0e45b026…"

The relay takes over the service's canonical container name by design — so label-driven commands keep treating it as the service — and the leftover replica still holds that name. The conflict is only the visible half of the problem: replicas 2..n of a scaled service would not collide by name, but they keep the service's network alias, competing with the relay on DNS and serving outdated traffic.

What the PR brings

The reconciler treats the migration as what it is: a replacement. For a provider-backed service, every observed container that is not the relay is condemned by the plan — stop, then remove — and the RunProvider node depends on those removals, so the canonical name is guaranteed free by the time the relay is created. The relay itself (identified by its RelayLabel) stays untouched: its convergence belongs to ensureServiceRelay, unchanged.

Guardrails:

  • Inert outside the migration case. A provider service with no leftover replicas plans exactly what it planned before (RunProvider alone); a standing up-to-date relay is left alone (locked by a dedicated unit test).
  • Both failure modes covered: the unit test locks the plan shape (stop → remove → provider, with the dependency edge), and a new e2e scenario runs the real migration — deploy as a regular container, switch the model, up — asserting the container is replaced (Recreated) and the relay serves at the compose-native address. Without the fix, that scenario fails exactly on the reported conflict.
  • The e2e Scenario DSL gains FromFile, a local mirror of FromRemote, so a scenario can switch its compose model between steps — the natural shape for migration tests.

@ndeloof
ndeloof requested review from a team as code owners September 25, 2026 13:26

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

Comment thread pkg/e2e/scenario.go
@ndeloof
ndeloof force-pushed the provider-migration-cleanup branch from 5021fbc to 85ce614 Compare September 25, 2026 13:37
glours
glours previously approved these changes Sep 25, 2026

@glours glours left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread pkg/compose/reconcile.go Outdated

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

A service migrated from regular replicas to a provider left its old
containers standing: not converged (a provider service has no replicas
to converge), not orphaned (the service is still in the model). The
first up after the migration then failed creating the relay, because
the leftover replica still held the service's canonical container name
the relay takes over. And even without the name conflict (replicas
2..n of a scaled service), stale replicas kept the service's network
alias, competing with the relay on DNS and serving outdated traffic.

The reconciler now plans a stop+remove for every observed non-relay
container of a provider service, and the RunProvider node depends on
those removals: the canonical name is free by the time
ensureServiceRelay creates the relay. The relay itself (RelayLabel) is
the provider service's legitimate container and is left alone —
converged by ensureServiceRelay, not by the plan.

The e2e scenario migrates a deployed service to a provider and locks
the replacement (Recreated, relay reachable at the compose-native
address); the Scenario DSL gains FromFile — a local mirror of
FromRemote — to switch the model between steps.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
@ndeloof
ndeloof enabled auto-merge (rebase) September 25, 2026 13:53

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

@ndeloof
ndeloof merged commit 32bddfc into docker:main Sep 25, 2026
62 checks passed
@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

3 participants