fix: recover Docker containers hidden from list - #7078
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses a Docker-compatible backend edge case where container creation fails with a name conflict, but a subsequent name-filtered container list returns no match (observed with rootless Podman). It adds a ContainerInspect fallback to “adopt” the existing Created-state container and proceed through the normal start/deploy path, with regression tests covering the conflict/list-miss scenario and inspect-to-summary field preservation.
Changes:
- Add
ContainerInspectfallback ingetContainerwhen name-filteredContainerListyields no match. - Convert inspect responses into the existing
container.Summaryshape so downstream deployment logic can reuse existing containers. - Add tests covering inspect fallback adoption and field preservation in the inspect-derived summary.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| pkg/mcp/docker.go | Adds inspect fallback in getContainer and introduces inspectResponseToSummary helper for adopting containers not returned by list. |
| pkg/mcp/docker_test.go | Adds regression tests for the name-conflict + list-miss scenario and for inspect-to-summary field preservation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if inspect.NetworkSettings != nil { | ||
| summary.NetworkSettings.Networks = inspect.NetworkSettings.Networks | ||
| } |
There was a problem hiding this comment.
Fixed in 3126636. The inspect conversion now derives Summary.Ports from NetworkSettings.Ports, including multiple host bindings and exposed ports without host bindings. Regression coverage verifies both cases.
| if inspect.Config != nil { | ||
| summary.Image = inspect.Config.Image | ||
| summary.Labels = maps.Clone(inspect.Config.Labels) | ||
| } |
There was a problem hiding this comment.
Fixed in 3126636. The initialized labels map is preserved when inspect Config.Labels is nil, and a regression test verifies the result remains writable.
| var startCalls atomic.Int32 | ||
|
|
||
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| path := strings.TrimPrefix(r.URL.Path, "/v1.52") |
There was a problem hiding this comment.
Fixed in 3126636. The mock now strips any numeric Docker API version prefix, and the regression test explicitly runs the client at API version 1.51.
be1cb8c to
3126636
Compare
| for _, binding := range bindings { | ||
| publicPort, _ := strconv.ParseUint(binding.HostPort, 10, 16) | ||
| summary.Ports = append(summary.Ports, container.Port{ | ||
| IP: binding.HostIP, | ||
| PrivatePort: privatePort, | ||
| PublicPort: uint16(publicPort), | ||
| Type: port.Proto(), | ||
| }) | ||
| } |
There was a problem hiding this comment.
Addressed in e428eb3. inspectResponseToSummary now skips bindings whose HostPort is empty or non-numeric, while preserving exposed ports that have no host bindings. The regression test includes both invalid cases and verifies that no synthetic PublicPort=0 entries are emitted.
| inspect, err := d.client.ContainerInspect(ctx, name) | ||
| if cerrdefs.IsNotFound(err) { | ||
| return nil, nil | ||
| } | ||
| if err != nil { |
There was a problem hiding this comment.
Addressed in e428eb3. getContainer remains list-only; exact-name ContainerInspect is now called only after ContainerCreate returns a conflict or already-exists error and the list lookup misses. TestGetContainerNotFoundDoesNotInspect locks the ordinary not-found path at zero inspect calls, while the original conflict regression still requires exactly one inspect.
3126636 to
e428eb3
Compare
| if inspect.NetworkSettings != nil { | ||
| summary.NetworkSettings.Networks = inspect.NetworkSettings.Networks | ||
| for port, bindings := range inspect.NetworkSettings.Ports { | ||
| privatePort := uint16(port.Int()) | ||
| if len(bindings) == 0 { | ||
| summary.Ports = append(summary.Ports, container.Port{ | ||
| PrivatePort: privatePort, | ||
| Type: port.Proto(), | ||
| }) | ||
| continue | ||
| } | ||
| for _, binding := range bindings { | ||
| publicPort, err := strconv.ParseUint(binding.HostPort, 10, 16) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| summary.Ports = append(summary.Ports, container.Port{ | ||
| IP: binding.HostIP, | ||
| PrivatePort: privatePort, | ||
| PublicPort: uint16(publicPort), | ||
| Type: port.Proto(), | ||
| }) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Addressed in 749d037. inspectResponseToSummary now orders ports deterministically by private port and protocol, prioritizes published bindings, and prefers IPv4 loopback/IPv4 before IPv6 with stable IP and public-port tie-breakers. TestInspectResponseToSummaryOrdersPublishedPorts starts from an IPv6-first binding list, asserts the full order, and verifies getHostPort selects the 127.0.0.1 binding.
| @@ -1071,6 +1176,12 @@ func (d *dockerBackend) createAndStartContainer(ctx context.Context, server Serv | |||
| if getErr != nil { | |||
| return "", 0, fmt.Errorf("failed to create container: %w", err) | |||
Summary
Refs #6543
Changes
ContainerInspectfallback when Docker-compatible backends report a create name conflict but the name-filtered container list returns no match.Scope and risk
The inspect fallback is limited to the existing bounded create-conflict retry path. It requires an exact container-name match and does not change Kubernetes behavior, public APIs, or runtime configuration.
Verification