diff --git a/pkg/compose/convergence.go b/pkg/compose/convergence.go index 063a24e73..aa89c2b35 100644 --- a/pkg/compose/convergence.go +++ b/pkg/compose/convergence.go @@ -448,7 +448,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types Networks: inspectedContainer.NetworkSettings.Networks, }, } - links := append(service.Links, service.ExternalLinks...) + links := s.getLinks(ctx, project.Name, service, number) for _, netName := range service.NetworksByPriority() { netwrk := project.Networks[netName] cfg := service.Networks[netName] @@ -476,6 +476,64 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types return created, err } +// getLinks mimics V1 compose/service.py::Service::_get_links() +func (s composeService) getLinks(ctx context.Context, projectName string, service types.ServiceConfig, number int) []string { + var links []string + format := func(k, v string) string { + return fmt.Sprintf("%s:%s", k, v) + } + getServiceContainers := func(serviceName string) (Containers, error) { + return s.getContainers(ctx, projectName, oneOffExclude, true, serviceName) + } + + for _, rawLink := range service.Links { + linkSplit := strings.Split(rawLink, ":") + linkServiceName := linkSplit[0] + l := linkServiceName + if len(linkSplit) == 2 { + l = linkSplit[1] // linkName if informed like in: "serviceName:linkName" + } + cnts, err := getServiceContainers(linkServiceName) + if err != nil { + return nil + } + for _, c := range cnts { + containerName := getCanonicalContainerName(c) + links = append(links, + format(containerName, l), + format(containerName, strings.Join([]string{linkServiceName, strconv.Itoa(number)}, Separator)), + format(containerName, strings.Join([]string{projectName, linkServiceName, strconv.Itoa(number)}, Separator)), + ) + } + } + + if service.Labels[api.OneoffLabel] == "True" { + cnts, err := getServiceContainers(service.Name) + if err != nil { + return nil + } + for _, c := range cnts { + containerName := getCanonicalContainerName(c) + links = append(links, + format(containerName, service.Name), + format(containerName, strings.TrimPrefix(containerName, projectName+Separator)), + format(containerName, containerName), + ) + } + } + + for _, rawExtLink := range service.ExternalLinks { + extLinkSplit := strings.Split(rawExtLink, ":") + externalLink := extLinkSplit[0] + linkName := externalLink + if len(extLinkSplit) == 2 { + linkName = extLinkSplit[1] + } + links = append(links, format(externalLink, linkName)) + } + return links +} + func shortIDAliasExists(containerID string, aliases ...string) bool { for _, alias := range aliases { if alias == containerID[:12] { diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index 5af583c11..f60a852e3 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -17,10 +17,16 @@ package compose import ( + "context" "fmt" "testing" "github.com/compose-spec/compose-go/types" + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/mocks" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/golang/mock/gomock" "gotest.tools/assert" ) @@ -46,3 +52,128 @@ func TestContainerName(t *testing.T) { _, err = getScale(s) assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName)) } + +func TestServiceLinks(t *testing.T) { + const dbContainerName = "/" + testProject + "-db-1" + const webContainerName = "/" + testProject + "-web-1" + s := types.ServiceConfig{ + Name: "web", + Scale: 1, + } + + containerListOptions := moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(testProject), + serviceFilter("db"), + oneOffFilter(false), + ), + All: true, + } + + t.Run("service links default", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = apiClient + + s.Links = []string{"db"} + + c := testContainer("db", dbContainerName, false) + apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil) + + links := tested.getLinks(context.Background(), testProject, s, 1) + + assert.Equal(t, len(links), 3) + assert.Equal(t, links[0], "testProject-db-1:db") + assert.Equal(t, links[1], "testProject-db-1:db-1") + assert.Equal(t, links[2], "testProject-db-1:testProject-db-1") + }) + + t.Run("service links", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = apiClient + + s.Links = []string{"db:db"} + + c := testContainer("db", dbContainerName, false) + + apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil) + links := tested.getLinks(context.Background(), testProject, s, 1) + + assert.Equal(t, len(links), 3) + assert.Equal(t, links[0], "testProject-db-1:db") + assert.Equal(t, links[1], "testProject-db-1:db-1") + assert.Equal(t, links[2], "testProject-db-1:testProject-db-1") + }) + + t.Run("service links name", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = apiClient + + s.Links = []string{"db:dbname"} + + c := testContainer("db", dbContainerName, false) + apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil) + + links := tested.getLinks(context.Background(), testProject, s, 1) + + assert.Equal(t, len(links), 3) + assert.Equal(t, links[0], "testProject-db-1:dbname") + assert.Equal(t, links[1], "testProject-db-1:db-1") + assert.Equal(t, links[2], "testProject-db-1:testProject-db-1") + }) + + t.Run("service links external links", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = apiClient + + s.Links = []string{"db:dbname"} + s.ExternalLinks = []string{"db1:db2"} + + c := testContainer("db", dbContainerName, false) + apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptions).Return([]moby.Container{c}, nil) + + links := tested.getLinks(context.Background(), testProject, s, 1) + assert.Equal(t, len(links), 4) + assert.Equal(t, links[0], "testProject-db-1:dbname") + assert.Equal(t, links[1], "testProject-db-1:db-1") + assert.Equal(t, links[2], "testProject-db-1:testProject-db-1") + + // ExternalLink + assert.Equal(t, links[3], "db1:db2") + }) + + t.Run("service links itself oneoff", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + apiClient := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = apiClient + + s.Links = []string{} + s.ExternalLinks = []string{} + s.Labels = s.Labels.Add(api.OneoffLabel, "True") + + c := testContainer("web", webContainerName, true) + containerListOptionsOneOff := moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(testProject), + serviceFilter("web"), + oneOffFilter(false), + ), + All: true, + } + apiClient.EXPECT().ContainerList(gomock.Any(), containerListOptionsOneOff).Return([]moby.Container{c}, nil) + + links := tested.getLinks(context.Background(), testProject, s, 1) + assert.Equal(t, len(links), 3) + assert.Equal(t, links[0], "testProject-web-1:web") + assert.Equal(t, links[1], "testProject-web-1:web-1") + assert.Equal(t, links[2], "testProject-web-1:testProject-web-1") + }) +}