From 49a4cb13b8548cdf8b33454150b84f281e8ba657 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Fri, 8 May 2026 11:47:57 +0300 Subject: [PATCH] fix: do not add service name as alias on external networks Previously, getAliases() unconditionally appended the service name as a network alias when useNetworkAliases was true. This caused containers to register their service name as an alias on external networks, leaking internal service discovery names into networks managed outside of Compose. Guard the service name alias behind an external-network check: only append it when the network is not marked as external. Explicitly configured aliases in the service network config are still passed through regardless. To make the behavior change discoverable, `up` now emits one warning per external network the project is connected to, listing the services whose service-name alias was skipped. Services that already declare their own name under networks..aliases are excluded, so the warning disappears once the user adopts the workaround. `create` and `run --use-aliases` are intentionally not warned about: `create` is rarely used standalone, and `run --use-aliases` produces ephemeral one-off containers where the warning would be noise. Fixes #8223 Signed-off-by: Stanislav Zhuk --- pkg/compose/create.go | 9 +- pkg/compose/create_test.go | 20 +++ pkg/compose/up.go | 34 +++++ pkg/compose/up_test.go | 133 ++++++++++++++++++ .../network-external-alias/compose.yaml | 21 +++ pkg/e2e/networks_test.go | 60 ++++++++ 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 pkg/e2e/fixtures/network-external-alias/compose.yaml diff --git a/pkg/compose/create.go b/pkg/compose/create.go index cc0b4afbc..0ea3368c9 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -368,10 +368,13 @@ func (s *composeService) prepareContainerMACAddress(service types.ServiceConfig, return nil } -func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, cfg *types.ServiceNetworkConfig, useNetworkAliases bool) []string { +func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, cfg *types.ServiceNetworkConfig, useNetworkAliases bool) []string { aliases := []string{getContainerName(project.Name, service, serviceIndex)} if useNetworkAliases { - aliases = append(aliases, service.Name) + // service name is not registered as an alias on external networks; see warnExternalNetworkAliases + if n := project.Networks[networkKey]; !n.External { + aliases = append(aliases, service.Name) + } if cfg != nil { aliases = append(aliases, cfg.Aliases...) } @@ -445,7 +448,7 @@ func createEndpointSettings(p *types.Project, service types.ServiceConfig, servi } return &network.EndpointSettings{ - Aliases: getAliases(p, service, serviceIndex, config, useNetworkAliases), + Aliases: getAliases(p, service, serviceIndex, networkKey, config, useNetworkAliases), Links: links, IPAddress: ipv4Address, IPv6Gateway: ipv6Address, diff --git a/pkg/compose/create_test.go b/pkg/compose/create_test.go index e08e4227d..7c3c2b730 100644 --- a/pkg/compose/create_test.go +++ b/pkg/compose/create_test.go @@ -365,6 +365,26 @@ func TestCreateEndpointSettings(t *testing.T) { }, cmpopts.EquateComparable(netip.Addr{}))) } +func TestCreateEndpointSettings_ExternalNetwork(t *testing.T) { + eps, err := createEndpointSettings(&composetypes.Project{ + Name: "projName", + Networks: composetypes.Networks{ + "netName": {External: true}, + }, + }, composetypes.ServiceConfig{ + Name: "serviceName", + ContainerName: "containerName", + Networks: map[string]*composetypes.ServiceNetworkConfig{ + "netName": { + Aliases: []string{"alias1"}, + }, + }, + }, 0, "netName", nil, true) + assert.NilError(t, err) + assert.Check(t, cmp.DeepEqual(eps.Aliases, []string{"containerName", "alias1"}), + "service name must not be added as alias on external networks") +} + func Test_buildContainerVolumes(t *testing.T) { pwd, err := os.Getwd() assert.NilError(t, err) diff --git a/pkg/compose/up.go b/pkg/compose/up.go index b2bcb3f7d..8cfccede8 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -23,6 +23,7 @@ import ( "os" "os/signal" "slices" + "strings" "sync" "sync/atomic" "syscall" @@ -42,6 +43,7 @@ import ( ) func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo + warnExternalNetworkAliases(project) err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { err := s.create(ctx, project, options.Create) if err != nil { @@ -314,3 +316,35 @@ func shouldFollowStartEvent(event api.ContainerEvent, attached []string, attachT } return true } + +// warnExternalNetworkAliases emits one warning per external network the project is connected to, listing the services +// whose service-name alias is intentionally not registered (see getAliases). Services that already include their own +// name under the network's explicit aliases are excluded, so the warning disappears once the user adopts the +// workaround. Must be called before Run() to avoid interleaving with the TUI progress display. +func warnExternalNetworkAliases(project *types.Project) { + byNetwork := map[string][]string{} + for _, service := range project.Services { + for networkKey, cfg := range service.Networks { + n := project.Networks[networkKey] + if !n.External { + continue + } + if cfg != nil && slices.Contains(cfg.Aliases, service.Name) { + continue + } + byNetwork[networkKey] = append(byNetwork[networkKey], service.Name) + } + } + networks := make([]string, 0, len(byNetwork)) + for k := range byNetwork { + networks = append(networks, k) + } + slices.Sort(networks) + for _, networkKey := range networks { + services := byNetwork[networkKey] + slices.Sort(services) + logrus.Warnf("service names [%s] not registered as aliases on external "+ + "network %q; add them under each service's networks.%s.aliases if needed", + strings.Join(services, ", "), networkKey, networkKey) + } +} diff --git a/pkg/compose/up_test.go b/pkg/compose/up_test.go index f38a9e1ae..842d7d40c 100644 --- a/pkg/compose/up_test.go +++ b/pkg/compose/up_test.go @@ -17,8 +17,12 @@ package compose import ( + "bytes" + "strings" "testing" + composetypes "github.com/compose-spec/compose-go/v2/types" + "github.com/sirupsen/logrus" "gotest.tools/v3/assert" "github.com/docker/compose/v5/pkg/api" @@ -102,3 +106,132 @@ func TestShouldFollowStartEvent(t *testing.T) { }) } } + +func TestWarnExternalNetworkAliases(t *testing.T) { + tests := []struct { + name string + project *composetypes.Project + expected []string + notExpected []string + }{ + { + name: "internal-only network emits no warning", + project: &composetypes.Project{ + Networks: composetypes.Networks{"internal": {}}, + Services: composetypes.Services{ + "web": { + Name: "web", + Networks: map[string]*composetypes.ServiceNetworkConfig{"internal": nil}, + }, + }, + }, + notExpected: []string{"not registered as aliases"}, + }, + { + name: "external network emits one warning listing services in sorted order", + project: &composetypes.Project{ + Networks: composetypes.Networks{"shared": {External: true}}, + Services: composetypes.Services{ + "web": { + Name: "web", + Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil}, + }, + "db": { + Name: "db", + Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil}, + }, + }, + }, + expected: []string{ + `service names [db, web] not registered as aliases on external network`, + `networks.shared.aliases`, + }, + }, + { + name: "service with explicit self-alias is excluded from the warning", + project: &composetypes.Project{ + Networks: composetypes.Networks{"shared": {External: true}}, + Services: composetypes.Services{ + "web": { + Name: "web", + Networks: map[string]*composetypes.ServiceNetworkConfig{ + "shared": {Aliases: []string{"web"}}, + }, + }, + "db": { + Name: "db", + Networks: map[string]*composetypes.ServiceNetworkConfig{"shared": nil}, + }, + }, + }, + expected: []string{ + `service names [db] not registered as aliases on external network`, + `networks.shared.aliases`, + }, + notExpected: []string{"web"}, + }, + { + name: "all services explicitly self-aliased emits no warning", + project: &composetypes.Project{ + Networks: composetypes.Networks{"shared": {External: true}}, + Services: composetypes.Services{ + "web": { + Name: "web", + Networks: map[string]*composetypes.ServiceNetworkConfig{ + "shared": {Aliases: []string{"web"}}, + }, + }, + }, + }, + notExpected: []string{"not registered as aliases"}, + }, + { + name: "multiple external networks each emit their own warning", + project: &composetypes.Project{ + Networks: composetypes.Networks{ + "sharedA": {External: true}, + "sharedB": {External: true}, + }, + Services: composetypes.Services{ + "web": { + Name: "web", + Networks: map[string]*composetypes.ServiceNetworkConfig{ + "sharedA": nil, + "sharedB": nil, + }, + }, + }, + }, + expected: []string{ + `networks.sharedA.aliases`, + `networks.sharedB.aliases`, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + origOut := logrus.StandardLogger().Out + origLevel := logrus.GetLevel() + origFormatter := logrus.StandardLogger().Formatter + logrus.SetOutput(&buf) + logrus.SetLevel(logrus.WarnLevel) + logrus.SetFormatter(&logrus.TextFormatter{DisableColors: true, DisableTimestamp: true}) + t.Cleanup(func() { + logrus.SetOutput(origOut) + logrus.SetLevel(origLevel) + logrus.SetFormatter(origFormatter) + }) + + warnExternalNetworkAliases(tt.project) + + out := buf.String() + for _, s := range tt.expected { + assert.Assert(t, strings.Contains(out, s), "expected %q in output:\n%s", s, out) + } + for _, s := range tt.notExpected { + assert.Assert(t, !strings.Contains(out, s), "did not expect %q in output:\n%s", s, out) + } + }) + } +} diff --git a/pkg/e2e/fixtures/network-external-alias/compose.yaml b/pkg/e2e/fixtures/network-external-alias/compose.yaml new file mode 100644 index 000000000..9b29b619a --- /dev/null +++ b/pkg/e2e/fixtures/network-external-alias/compose.yaml @@ -0,0 +1,21 @@ +networks: + default: {} + external-net: + external: true + name: ${EXTERNAL_NETWORK} + +services: + web: + image: busybox + command: sleep infinity + networks: + - default + - external-net + db: + image: busybox + command: sleep infinity + networks: + default: {} + external-net: + aliases: + - db diff --git a/pkg/e2e/networks_test.go b/pkg/e2e/networks_test.go index d4fbc3659..3d65be8dc 100644 --- a/pkg/e2e/networks_test.go +++ b/pkg/e2e/networks_test.go @@ -219,3 +219,63 @@ func TestNetworkRecreate(t *testing.T) { t.Fatalf("unexpected output, missing expected events, stderr: %s", err) } } + +func TestExternalNetworkAliases(t *testing.T) { + const projectName = "network_external_alias_e2e" + const externalNet = projectName + "_external" + + c := NewParallelCLI(t, WithEnv("EXTERNAL_NETWORK="+externalNet)) + + c.RunDockerOrExitError(t, "network", "rm", externalNet) + c.RunDockerCmd(t, "network", "create", externalNet) + t.Cleanup(func() { + c.cleanupWithDown(t, projectName) + c.RunDockerOrExitError(t, "network", "rm", externalNet) + }) + + upRes := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-external-alias/compose.yaml", + "--project-name", projectName, + "up", "-d") + + internalNet := projectName + "_default" + + t.Run("warning lists services without explicit external-net alias and excludes self-aliased ones", func(t *testing.T) { + var warningLine string + for line := range strings.SplitSeq(upRes.Combined(), "\n") { + if strings.Contains(line, `not registered as aliases on external network`) { + warningLine = line + break + } + } + assert.Assert(t, warningLine != "", "expected warning line in output:\n%s", upRes.Combined()) + assert.Assert(t, strings.Contains(warningLine, "web"), warningLine) + assert.Assert(t, strings.Contains(warningLine, "external-net"), warningLine) + assert.Assert(t, !strings.Contains(warningLine, "db"), + "db declares its own external-net alias and must be excluded from the warning: %s", warningLine) + }) + + t.Run("service name is an alias on internal network", func(t *testing.T) { + res := c.RunDockerCmd(t, "inspect", projectName+"-web-1", "-f", + fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, internalNet)) + assert.Assert(t, strings.Contains(res.Stdout(), "[web]"), res.Stdout()) + }) + + t.Run("service name is not an alias on external network", func(t *testing.T) { + res := c.RunDockerCmd(t, "inspect", projectName+"-web-1", "-f", + fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, externalNet)) + assert.Assert(t, !strings.Contains(res.Stdout(), "[web]"), res.Stdout()) + }) + + t.Run("explicit alias under networks..aliases is registered on external network", func(t *testing.T) { + res := c.RunDockerCmd(t, "inspect", projectName+"-db-1", "-f", + fmt.Sprintf(`{{ range (index .NetworkSettings.Networks %q).Aliases }}[{{ . }}]{{ end }}`, externalNet)) + assert.Assert(t, strings.Contains(res.Stdout(), "[db]"), res.Stdout()) + }) + + t.Run("service name resolves on internal network", func(t *testing.T) { + res := c.RunDockerComposeCmd(t, "-f", "./fixtures/network-external-alias/compose.yaml", + "--project-name", projectName, + "exec", "-T", "web", "ping", "-c1", "db") + assert.Assert(t, strings.Contains(res.Combined(), "1 packets transmitted"), res.Combined()) + }) +}