Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkg/compose/api_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ package compose
// Docker Engine API version constants.
// These versions correspond to specific Docker Engine releases and their features.
const (
// apiVersion144 represents Docker Engine API version 1.44 (Engine v23.0).
//
// New features in this version:
// - ContainerCreate API accepts multiple EndpointsConfig entries
//
// Before this version:
// - Only a single EndpointsConfig entry was accepted in ContainerCreate
// - Extra networks must be connected individually after container creation via NetworkConnect
apiVersion144 = "1.44"

// apiVersion148 represents Docker Engine API version 1.48 (Engine v28.0).
//
// New features in this version:
Expand Down
30 changes: 30 additions & 0 deletions pkg/compose/convergence.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/moby/moby/api/types/container"
mmount "github.com/moby/moby/api/types/mount"
"github.com/moby/moby/client"
"github.com/moby/moby/client/pkg/versions"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -732,6 +733,35 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
Text: warning,
})
}

// Starting API version 1.44, the ContainerCreate API call takes multiple networks
// so we include all configurations there and can skip the one-by-one calls here.
// For older API versions (e.g. Docker 20.10/API 1.41, Synology DSM 7.1/7.2),
// extra networks must be connected individually after creation via NetworkConnect.
if versions.LessThan(cfgs.APIVersion, apiVersion144) {
// The highest-priority network is the primary and is already included in the
// ContainerCreate API call via NetworkMode & NetworkingConfig.
// Any remaining networks are connected one-by-one here after creation (but before start).
serviceNetworks := service.NetworksByPriority()
for _, networkKey := range serviceNetworks {
mobyNetworkName := project.Networks[networkKey].Name
if string(cfgs.Host.NetworkMode) == mobyNetworkName {
// primary network already configured as part of ContainerCreate
continue
}
epSettings, err := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
if err != nil {
return created, err
}
if _, err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, client.NetworkConnectOptions{
Container: response.ID,
EndpointConfig: epSettings,
}); err != nil {
return created, err
}
}
}

res, err := s.apiClient().ContainerInspect(ctx, response.ID, client.ContainerInspectOptions{})
if err != nil {
return created, err
Expand Down
95 changes: 95 additions & 0 deletions pkg/compose/convergence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,98 @@ func TestCreateMobyContainer(t *testing.T) {
assert.DeepEqual(t, want, got, cmpopts.EquateComparable(netip.Addr{}), cmpopts.EquateEmpty())
assert.NilError(t, err)
}

func TestCreateMobyContainerLegacyAPI(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
apiClient := mocks.NewMockAPIClient(mockCtrl)
cli := mocks.NewMockCli(mockCtrl)
tested, err := NewComposeService(cli)
assert.NilError(t, err)
cli.EXPECT().Client().Return(apiClient).AnyTimes()
cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
apiClient.EXPECT().DaemonHost().Return("").AnyTimes()
apiClient.EXPECT().ImageInspect(anyCancellableContext(), gomock.Any()).Return(client.ImageInspectResult{}, nil).AnyTimes()

// force `RuntimeVersion` to return a pre-1.44 API version
runtimeVersion = runtimeVersionCache{}
apiClient.EXPECT().ServerVersion(gomock.Any(), gomock.Any()).Return(client.ServerVersionResult{
APIVersion: "1.43",
}, nil).AnyTimes()

service := types.ServiceConfig{
Name: "test",
Networks: map[string]*types.ServiceNetworkConfig{
"a": {
Priority: 10,
},
"b": {
Priority: 100,
},
},
}
project := types.Project{
Name: "bork",
Services: types.Services{
"test": service,
},
Networks: types.Networks{
"a": types.NetworkConfig{
Name: "a-moby-name",
},
"b": types.NetworkConfig{
Name: "b-moby-name",
},
},
}

var gotCreate client.ContainerCreateOptions
apiClient.EXPECT().ContainerCreate(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, opts client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
gotCreate = opts
return client.ContainerCreateResult{ID: "an-id"}, nil
})

// For API < 1.44, the secondary network "a" should be connected via NetworkConnect.
// Using gomock.InOrder to verify NetworkConnect is called before ContainerInspect.
var gotConnect client.NetworkConnectOptions
connectCall := apiClient.EXPECT().NetworkConnect(gomock.Any(), gomock.Eq("a-moby-name"), gomock.Any()).DoAndReturn(func(_ context.Context, _ string, opts client.NetworkConnectOptions) (client.NetworkConnectResult, error) {
gotConnect = opts
return client.NetworkConnectResult{}, nil
})

apiClient.EXPECT().ContainerInspect(gomock.Any(), gomock.Eq("an-id"), gomock.Any()).Times(1).After(connectCall).Return(client.ContainerInspectResult{
Container: container.InspectResponse{
ID: "an-id",
Name: "a-name",
Config: &container.Config{},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
"b-moby-name": {
IPAMConfig: &network.EndpointIPAMConfig{},
Aliases: []string{"bork-test-0"},
},
"a-moby-name": {
IPAMConfig: &network.EndpointIPAMConfig{},
Aliases: []string{"bork-test-0"},
},
},
},
},
}, nil)

_, err = tested.(*composeService).createMobyContainer(t.Context(), &project, service, "test", 0, nil, createOptions{
Labels: make(types.Labels),
UseNetworkAliases: true,
})
assert.NilError(t, err)

// ContainerCreate should only have the primary network (b, highest priority) in EndpointsConfig
assert.Check(t, gotCreate.NetworkingConfig != nil)
assert.Equal(t, len(gotCreate.NetworkingConfig.EndpointsConfig), 1)
_, hasPrimary := gotCreate.NetworkingConfig.EndpointsConfig["b-moby-name"]
assert.Check(t, hasPrimary, "primary network b-moby-name should be in ContainerCreate EndpointsConfig")

// NetworkConnect should have been called for the secondary network "a"
assert.Equal(t, gotConnect.Container, "an-id")
assert.Check(t, gotConnect.EndpointConfig != nil)
}
34 changes: 20 additions & 14 deletions pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ type createOptions struct {
}

type createConfigs struct {
Container *container.Config
Host *container.HostConfig
Network *network.NetworkingConfig
Links []string
Container *container.Config
Host *container.HostConfig
Network *network.NetworkingConfig
Links []string
APIVersion string
}

func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error {
Expand Down Expand Up @@ -332,10 +333,11 @@ func (s *composeService) getCreateConfigs(ctx context.Context,
}

cfgs := createConfigs{
Container: &containerConfig,
Host: &hostConfig,
Network: networkingConfig,
Links: links,
Container: &containerConfig,
Host: &hostConfig,
Network: networkingConfig,
Links: links,
APIVersion: apiVersion,
}
return cfgs, nil
}
Expand Down Expand Up @@ -563,13 +565,17 @@ func defaultNetworkSettings(project *types.Project,
// so we can pass all the extra networks we want the container to be connected to
// in the network configuration instead of connecting the container to each extra
// network individually after creation.
for _, networkKey := range serviceNetworks {
epSettings, err := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
if err != nil {
return "", nil, err
// For older API versions, extra networks are connected via NetworkConnect after
// container creation (see createMobyContainer in convergence.go).
if !versions.LessThan(version, apiVersion144) {
for _, networkKey := range serviceNetworks {
epSettings, err := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
if err != nil {
return "", nil, err
}
mobyNetworkName := project.Networks[networkKey].Name
endpointsConfig[mobyNetworkName] = epSettings
}
mobyNetworkName := project.Networks[networkKey].Name
endpointsConfig[mobyNetworkName] = epSettings
}

networkConfig := &network.NetworkingConfig{
Expand Down
34 changes: 34 additions & 0 deletions pkg/compose/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,40 @@ func TestDefaultNetworkSettings(t *testing.T) {
assert.Check(t, cmp.Nil(networkConfig))
})

t.Run("returns only primary network in EndpointsConfig for API < 1.44", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
Networks: map[string]*composetypes.ServiceNetworkConfig{
"myNetwork1": {
Priority: 10,
},
"myNetwork2": {
Priority: 1000,
},
},
}
project := composetypes.Project{
Name: "myProject",
Services: composetypes.Services{
"myService": service,
},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"myNetwork1": {
Name: "myProject_myNetwork1",
},
"myNetwork2": {
Name: "myProject_myNetwork2",
},
}),
}

networkMode, networkConfig, err := defaultNetworkSettings(&project, service, 1, nil, true, "1.43")
assert.NilError(t, err)
assert.Equal(t, string(networkMode), "myProject_myNetwork2")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_myNetwork2"))
})

t.Run("returns defined network mode if explicitly set", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
Expand Down