Skip to content

Commit 0f74741

Browse files
committed
networks: prevent issues due to duplicate names
Within the Docker API/engine, networks have unique IDs, and the name is a friendly label/alias, which notably does NOT have any guarantees around uniqueness (see moby/moby#18864 [^1]). During day-to-day interactive/CLI Compose usage, this is rarely an issue, as Compose itself isn't creating networks concurrently across goroutines. However, if multiple Compose instances are executed simultaneously (e.g. as part of a test suite that runs in parallel), this can easily occur. When it does happen, it's very confusing for users and resolving it via the `docker` CLI is not straightforward either [^2]. There's two primary changes here: * Pass `CheckDuplicates: true` to the Docker API when creating networks to reduce the likelihood of Compose creating duplicates in the first place * On `down`, list networks using a name filter and then remove them all by ID, as the Docker API will return an error if the name alias is used and maps to more than one network Hopefully, this provides a better UX, since the issue should be less likely to occur, and if it does, it can now be resolved via standard Compose workflow commands. [^1]: moby/moby#18864 [^2]: https://stackoverflow.com/questions/63776518/error-2-matches-found-based-on-name-network-nameofservice-default-is-ambiguo Signed-off-by: Milas Bowman <[email protected]>
1 parent e5dcb8a commit 0f74741

File tree

3 files changed

+65
-25
lines changed

3 files changed

+65
-25
lines changed

pkg/compose/create.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
10701070
}
10711071
}
10721072
createOpts := moby.NetworkCreate{
1073+
CheckDuplicate: true,
10731074
// TODO NameSpace Labels
10741075
Labels: n.Labels,
10751076
Driver: n.Driver,
@@ -1110,19 +1111,6 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
11101111
return nil
11111112
}
11121113

1113-
func (s *composeService) removeNetwork(ctx context.Context, network string, w progress.Writer) error {
1114-
eventName := fmt.Sprintf("Network %s", network)
1115-
w.Event(progress.RemovingEvent(eventName))
1116-
1117-
if err := s.apiClient().NetworkRemove(ctx, network); err != nil {
1118-
w.Event(progress.ErrorEvent(eventName))
1119-
return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", network))
1120-
}
1121-
1122-
w.Event(progress.RemovedEvent(eventName))
1123-
return nil
1124-
}
1125-
11261114
func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
11271115
inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
11281116
if err != nil {

pkg/compose/down.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import (
2323
"time"
2424

2525
"github.com/compose-spec/compose-go/types"
26-
"github.com/docker/cli/cli/registry/client"
2726
moby "github.com/docker/docker/api/types"
27+
"github.com/docker/docker/api/types/filters"
2828
"github.com/docker/docker/errdefs"
29+
"github.com/pkg/errors"
2930
"golang.org/x/sync/errgroup"
3031

3132
"github.com/docker/compose/v2/pkg/api"
@@ -131,19 +132,59 @@ func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.
131132
if n.External.External {
132133
continue
133134
}
135+
// loop capture variable for op closure
134136
networkName := n.Name
135-
_, err := s.apiClient().NetworkInspect(ctx, networkName, moby.NetworkInspectOptions{})
136-
if client.IsNotFound(err) {
137-
return nil
138-
}
139-
140137
ops = append(ops, func() error {
141138
return s.removeNetwork(ctx, networkName, w)
142139
})
143140
}
144141
return ops
145142
}
146143

144+
func (s *composeService) removeNetwork(ctx context.Context, name string, w progress.Writer) error {
145+
// networks are guaranteed to have unique IDs but NOT names, so it's
146+
// possible to get into a situation where a compose down will fail with
147+
// an error along the lines of:
148+
// failed to remove network test: Error response from daemon: network test is ambiguous (2 matches found based on name)
149+
// as a workaround here, the delete is done by ID after doing a list using
150+
// the name as a filter (99.9% of the time this will return a single result)
151+
networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
152+
Filters: filters.NewArgs(filters.Arg("name", name)),
153+
})
154+
if err != nil {
155+
return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
156+
}
157+
if len(networks) == 0 {
158+
return nil
159+
}
160+
161+
eventName := fmt.Sprintf("Network %s", name)
162+
w.Event(progress.RemovingEvent(eventName))
163+
164+
var removed int
165+
for _, net := range networks {
166+
if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
167+
if errdefs.IsNotFound(err) {
168+
continue
169+
}
170+
w.Event(progress.ErrorEvent(eventName))
171+
return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
172+
}
173+
removed++
174+
}
175+
176+
if removed == 0 {
177+
// in practice, it's extremely unlikely for this to ever occur, as it'd
178+
// mean the network was present when we queried at the start of this
179+
// method but was then deleted by something else in the interim
180+
w.Event(progress.NewEvent(eventName, progress.Done, "Warning: No resource found to remove"))
181+
return nil
182+
}
183+
184+
w.Event(progress.RemovedEvent(eventName))
185+
return nil
186+
}
187+
147188
func (s *composeService) getServiceImages(options api.DownOptions, project *types.Project) map[string]struct{} {
148189
images := map[string]struct{}{}
149190
for _, service := range project.Services {

pkg/compose/down_test.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ import (
2121
"strings"
2222
"testing"
2323

24-
compose "github.com/docker/compose/v2/pkg/api"
25-
"github.com/docker/compose/v2/pkg/mocks"
2624
moby "github.com/docker/docker/api/types"
2725
"github.com/docker/docker/api/types/filters"
2826
"github.com/docker/docker/api/types/volume"
2927
"github.com/golang/mock/gomock"
3028
"gotest.tools/v3/assert"
29+
30+
compose "github.com/docker/compose/v2/pkg/api"
31+
"github.com/docker/compose/v2/pkg/mocks"
3132
)
3233

3334
func TestDown(t *testing.T) {
@@ -59,8 +60,16 @@ func TestDown(t *testing.T) {
5960
api.EXPECT().ContainerRemove(gomock.Any(), "456", moby.ContainerRemoveOptions{Force: true}).Return(nil)
6061
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
6162

62-
api.EXPECT().NetworkInspect(gomock.Any(), "myProject_default", moby.NetworkInspectOptions{}).Return(moby.NetworkResource{Name: "myProject_default"}, nil)
63-
api.EXPECT().NetworkRemove(gomock.Any(), "myProject_default").Return(nil)
63+
// network names are not guaranteed to be unique, ensure Compose handles
64+
// cleanup properly if duplicates are inadvertently created
65+
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
66+
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
67+
}).Return([]moby.NetworkResource{
68+
{ID: "abc123", Name: "myProject_default"},
69+
{ID: "def456", Name: "myProject_default"},
70+
}, nil)
71+
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
72+
api.EXPECT().NetworkRemove(gomock.Any(), "def456").Return(nil)
6473

6574
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{})
6675
assert.NilError(t, err)
@@ -94,8 +103,10 @@ func TestDownRemoveOrphans(t *testing.T) {
94103
api.EXPECT().ContainerRemove(gomock.Any(), "789", moby.ContainerRemoveOptions{Force: true}).Return(nil)
95104
api.EXPECT().ContainerRemove(gomock.Any(), "321", moby.ContainerRemoveOptions{Force: true}).Return(nil)
96105

97-
api.EXPECT().NetworkInspect(gomock.Any(), "myProject_default", moby.NetworkInspectOptions{}).Return(moby.NetworkResource{Name: "myProject_default"}, nil)
98-
api.EXPECT().NetworkRemove(gomock.Any(), "myProject_default").Return(nil)
106+
api.EXPECT().NetworkList(gomock.Any(), moby.NetworkListOptions{
107+
Filters: filters.NewArgs(filters.Arg("name", "myProject_default")),
108+
}).Return([]moby.NetworkResource{{ID: "abc123", Name: "myProject_default"}}, nil)
109+
api.EXPECT().NetworkRemove(gomock.Any(), "abc123").Return(nil)
99110

100111
err := tested.Down(context.Background(), strings.ToLower(testProject), compose.DownOptions{RemoveOrphans: true})
101112
assert.NilError(t, err)

0 commit comments

Comments
 (0)