Merge pull request #10756 from milas/network-race

up: fix race condition on network connect
This commit is contained in:
Guillaume Lours 2023-06-29 22:47:29 +02:00 committed by GitHub
commit 02284378bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 160 deletions

View File

@ -29,7 +29,6 @@ import (
"github.com/containerd/containerd/platforms" "github.com/containerd/containerd/platforms"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
containerType "github.com/docker/docker/api/types/container" containerType "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
specs "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -233,7 +232,13 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
name := getContainerName(project.Name, service, number) name := getContainerName(project.Name, service, number)
i := i i := i
eg.Go(func() error { eg.Go(func() error {
container, err := c.service.createContainer(ctx, project, service, name, number, false, true, false) opts := createOptions{
AutoRemove: false,
AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
updated[actual+i] = container updated[actual+i] = container
return err return err
}) })
@ -399,12 +404,11 @@ func getScale(config types.ServiceConfig) (int, error) {
} }
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
name string, number int, autoRemove bool, useNetworkAliases bool, attachStdin bool) (container moby.Container, err error) { name string, number int, opts createOptions) (container moby.Container, err error) {
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
eventName := "Container " + name eventName := "Container " + name
w.Event(progress.CreatingEvent(eventName)) w.Event(progress.CreatingEvent(eventName))
container, err = s.createMobyContainer(ctx, project, service, name, number, nil, container, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts, w)
autoRemove, useNetworkAliases, attachStdin, w, mergeLabels(service.Labels, service.CustomLabels))
if err != nil { if err != nil {
return return
} }
@ -429,9 +433,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
} }
name := getContainerName(project.Name, service, number) name := getContainerName(project.Name, service, number)
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name) tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts := createOptions{
false, true, false, w, AutoRemove: false,
mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID)) AttachStdin: false,
UseNetworkAliases: true,
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
}
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -484,19 +492,18 @@ func (s *composeService) startContainer(ctx context.Context, container moby.Cont
return nil return nil
} }
func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyclo func (s *composeService) createMobyContainer(ctx context.Context,
project *types.Project, project *types.Project,
service types.ServiceConfig, service types.ServiceConfig,
name string, name string,
number int, number int,
inherit *moby.Container, inherit *moby.Container,
autoRemove, useNetworkAliases, attachStdin bool, opts createOptions,
w progress.Writer, w progress.Writer,
labels types.Labels,
) (moby.Container, error) { ) (moby.Container, error) {
var created moby.Container var created moby.Container
containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit, cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
autoRemove, attachStdin, labels)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -514,18 +521,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyc
plat = &p plat = &p
} }
links, err := s.getLinks(ctx, project.Name, service, number) response, err := s.apiClient().ContainerCreate(ctx, cfgs.Container, cfgs.Host, cfgs.Network, plat, name)
if err != nil {
return created, err
}
if networkingConfig != nil {
for k, s := range networkingConfig.EndpointsConfig {
s.Links = links
networkingConfig.EndpointsConfig[k] = s
}
}
response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
if err != nil { if err != nil {
return created, err return created, err
} }
@ -548,29 +544,19 @@ func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyc
Networks: inspectedContainer.NetworkSettings.Networks, Networks: inspectedContainer.NetworkSettings.Networks,
}, },
} }
for _, netName := range service.NetworksByPriority() {
netwrk := project.Networks[netName] // the highest-priority network is the primary and is included in the ContainerCreate API
cfg := service.Networks[netName] // call via container.NetworkMode & network.NetworkingConfig
aliases := []string{getContainerName(project.Name, service, number)} // any remaining networks are connected one-by-one here after creation (but before start)
if useNetworkAliases { serviceNetworks := service.NetworksByPriority()
aliases = append(aliases, service.Name) if len(serviceNetworks) > 1 {
if cfg != nil { for _, networkKey := range serviceNetworks[1:] {
aliases = append(aliases, cfg.Aliases...) mobyNetworkName := project.Networks[networkKey].Name
} epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
} if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
if val, ok := created.NetworkSettings.Networks[netwrk.Name]; ok {
if shortIDAliasExists(created.ID, val.Aliases...) {
continue
}
err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
if err != nil {
return created, err return created, err
} }
} }
err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, cfg, links, aliases...)
if err != nil {
return created, err
}
} }
err = s.injectSecrets(ctx, project, service, created.ID) err = s.injectSecrets(ctx, project, service, created.ID)
@ -635,43 +621,6 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
return links, nil return links, nil
} }
func shortIDAliasExists(containerID string, aliases ...string) bool {
for _, alias := range aliases {
if alias == containerID[:12] {
return true
}
}
return false
}
func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, netwrk string, cfg *types.ServiceNetworkConfig, links []string, aliases ...string) error {
var (
ipv4Address string
ipv6Address string
ipam *network.EndpointIPAMConfig
)
if cfg != nil {
ipv4Address = cfg.Ipv4Address
ipv6Address = cfg.Ipv6Address
ipam = &network.EndpointIPAMConfig{
IPv4Address: ipv4Address,
IPv6Address: ipv6Address,
LinkLocalIPs: cfg.LinkLocalIPs,
}
}
err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
Aliases: aliases,
IPAddress: ipv4Address,
GlobalIPv6Address: ipv6Address,
Links: links,
IPAMConfig: ipam,
})
if err != nil {
return err
}
return nil
}
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) { func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
for _, c := range containers { for _, c := range containers {
container, err := s.apiClient().ContainerInspect(ctx, c.ID) container, err := s.apiClient().ContainerInspect(ctx, c.ID)

View File

@ -48,6 +48,20 @@ import (
"github.com/docker/compose/v2/pkg/utils" "github.com/docker/compose/v2/pkg/utils"
) )
type createOptions struct {
AutoRemove bool
AttachStdin bool
UseNetworkAliases bool
Labels types.Labels
}
type createConfigs struct {
Container *container.Config
Host *container.HostConfig
Network *network.NetworkingConfig
Links []string
}
func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error { func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
return progress.RunWithTitle(ctx, func(ctx context.Context) error { return progress.RunWithTitle(ctx, func(ctx context.Context) error {
return s.create(ctx, project, options) return s.create(ctx, project, options)
@ -166,18 +180,16 @@ func (s *composeService) ensureProjectVolumes(ctx context.Context, project *type
return nil return nil
} }
func (s *composeService) getCreateOptions(ctx context.Context, func (s *composeService) getCreateConfigs(ctx context.Context,
p *types.Project, p *types.Project,
service types.ServiceConfig, service types.ServiceConfig,
number int, number int,
inherit *moby.Container, inherit *moby.Container,
autoRemove, attachStdin bool, opts createOptions,
labels types.Labels, ) (createConfigs, error) {
) (*container.Config, *container.HostConfig, *network.NetworkingConfig, error) { labels, err := s.prepareLabels(opts.Labels, service, number)
labels, err := s.prepareLabels(labels, service, number)
if err != nil { if err != nil {
return nil, nil, nil, err return createConfigs{}, err
} }
var ( var (
@ -196,11 +208,6 @@ func (s *composeService) getCreateOptions(ctx context.Context,
stdinOpen = service.StdinOpen stdinOpen = service.StdinOpen
) )
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return nil, nil, nil, err
}
proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil)) proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
env := proxyConfig.OverrideBy(service.Environment) env := proxyConfig.OverrideBy(service.Environment)
@ -211,8 +218,8 @@ func (s *composeService) getCreateOptions(ctx context.Context,
ExposedPorts: buildContainerPorts(service), ExposedPorts: buildContainerPorts(service),
Tty: tty, Tty: tty,
OpenStdin: stdinOpen, OpenStdin: stdinOpen,
StdinOnce: attachStdin && stdinOpen, StdinOnce: opts.AttachStdin && stdinOpen,
AttachStdin: attachStdin, AttachStdin: opts.AttachStdin,
AttachStderr: true, AttachStderr: true,
AttachStdout: true, AttachStdout: true,
Cmd: runCmd, Cmd: runCmd,
@ -228,20 +235,7 @@ func (s *composeService) getCreateOptions(ctx context.Context,
StopTimeout: ToSeconds(service.StopGracePeriod), StopTimeout: ToSeconds(service.StopGracePeriod),
} }
portBindings := buildContainerPortBindingOptions(service) // VOLUMES/MOUNTS/FILESYSTEMS
resources := getDeployResources(service)
if service.NetworkMode == "" {
service.NetworkMode = getDefaultNetworkMode(p, service)
}
var networkConfig *network.NetworkingConfig
for _, id := range service.NetworksByPriority() {
networkConfig = s.createNetworkConfig(p, service, id)
break
}
tmpfs := map[string]string{} tmpfs := map[string]string{}
for _, t := range service.Tmpfs { for _, t := range service.Tmpfs {
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
@ -250,7 +244,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
tmpfs[arr[0]] = "" tmpfs[arr[0]] = ""
} }
} }
binds, mounts, err := s.buildContainerVolumes(ctx, *p, service, inherit)
if err != nil {
return createConfigs{}, err
}
var volumesFrom []string
for _, v := range service.VolumesFrom {
if !strings.HasPrefix(v, "container:") {
return createConfigs{}, fmt.Errorf("invalid volume_from: %s", v)
}
volumesFrom = append(volumesFrom, v[len("container:"):])
}
// NETWORKING
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return createConfigs{}, err
}
networkMode, networkingConfig := defaultNetworkSettings(p, service, number, links, opts.UseNetworkAliases)
portBindings := buildContainerPortBindingOptions(service)
// MISC
resources := getDeployResources(service)
var logConfig container.LogConfig var logConfig container.LogConfig
if service.Logging != nil { if service.Logging != nil {
logConfig = container.LogConfig{ logConfig = container.LogConfig{
@ -258,31 +273,18 @@ func (s *composeService) getCreateOptions(ctx context.Context,
Config: service.Logging.Options, Config: service.Logging.Options,
} }
} }
var volumesFrom []string
for _, v := range service.VolumesFrom {
if !strings.HasPrefix(v, "container:") {
return nil, nil, nil, fmt.Errorf("invalid volume_from: %s", v)
}
volumesFrom = append(volumesFrom, v[len("container:"):])
}
links, err := s.getLinks(ctx, p.Name, service, number)
if err != nil {
return nil, nil, nil, err
}
securityOpts, unconfined, err := parseSecurityOpts(p, service.SecurityOpt) securityOpts, unconfined, err := parseSecurityOpts(p, service.SecurityOpt)
if err != nil { if err != nil {
return nil, nil, nil, err return createConfigs{}, err
} }
hostConfig := container.HostConfig{ hostConfig := container.HostConfig{
AutoRemove: autoRemove, AutoRemove: opts.AutoRemove,
Binds: binds, Binds: binds,
Mounts: mounts, Mounts: mounts,
CapAdd: strslice.StrSlice(service.CapAdd), CapAdd: strslice.StrSlice(service.CapAdd),
CapDrop: strslice.StrSlice(service.CapDrop), CapDrop: strslice.StrSlice(service.CapDrop),
NetworkMode: container.NetworkMode(service.NetworkMode), NetworkMode: networkMode,
Init: service.Init, Init: service.Init,
IpcMode: container.IpcMode(service.Ipc), IpcMode: container.IpcMode(service.Ipc),
CgroupnsMode: container.CgroupnsMode(service.Cgroup), CgroupnsMode: container.CgroupnsMode(service.Cgroup),
@ -317,12 +319,28 @@ func (s *composeService) getCreateOptions(ctx context.Context,
hostConfig.ReadonlyPaths = []string{} hostConfig.ReadonlyPaths = []string{}
} }
return &containerConfig, &hostConfig, networkConfig, nil cfgs := createConfigs{
Container: &containerConfig,
Host: &hostConfig,
Network: networkingConfig,
Links: links,
}
return cfgs, nil
} }
func (s *composeService) createNetworkConfig(p *types.Project, service types.ServiceConfig, networkID string) *network.NetworkingConfig { func getAliases(project *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, useNetworkAliases bool) []string {
net := p.Networks[networkID] aliases := []string{getContainerName(project.Name, service, serviceIndex)}
config := service.Networks[networkID] if useNetworkAliases {
aliases = append(aliases, service.Name)
if cfg := service.Networks[networkKey]; cfg != nil {
aliases = append(aliases, cfg.Aliases...)
}
}
return aliases
}
func createEndpointSettings(p *types.Project, service types.ServiceConfig, serviceIndex int, networkKey string, links []string, useNetworkAliases bool) *network.EndpointSettings {
config := service.Networks[networkKey]
var ipam *network.EndpointIPAMConfig var ipam *network.EndpointIPAMConfig
var ( var (
ipv4Address string ipv4Address string
@ -337,15 +355,12 @@ func (s *composeService) createNetworkConfig(p *types.Project, service types.Ser
LinkLocalIPs: config.LinkLocalIPs, LinkLocalIPs: config.LinkLocalIPs,
} }
} }
return &network.NetworkingConfig{ return &network.EndpointSettings{
EndpointsConfig: map[string]*network.EndpointSettings{ Aliases: getAliases(p, service, serviceIndex, networkKey, useNetworkAliases),
net.Name: { Links: links,
Aliases: getAliases(service, config),
IPAddress: ipv4Address, IPAddress: ipv4Address,
IPv6Gateway: ipv6Address, IPv6Gateway: ipv6Address,
IPAMConfig: ipam, IPAMConfig: ipam,
},
},
} }
} }
@ -404,17 +419,39 @@ func (s *composeService) prepareLabels(labels types.Labels, service types.Servic
return labels, nil return labels, nil
} }
func getDefaultNetworkMode(project *types.Project, service types.ServiceConfig) string { // defaultNetworkSettings determines the container.NetworkMode and corresponding network.NetworkingConfig (nil if not applicable).
func defaultNetworkSettings(
project *types.Project,
service types.ServiceConfig,
serviceIndex int,
links []string,
useNetworkAliases bool,
) (container.NetworkMode, *network.NetworkingConfig) {
if service.NetworkMode != "" {
return container.NetworkMode(service.NetworkMode), nil
}
if len(project.Networks) == 0 { if len(project.Networks) == 0 {
return "none" return "none", nil
} }
var networkKey string
if len(service.Networks) > 0 { if len(service.Networks) > 0 {
name := service.NetworksByPriority()[0] networkKey = service.NetworksByPriority()[0]
return project.Networks[name].Name } else {
networkKey = "default"
} }
mobyNetworkName := project.Networks[networkKey].Name
return project.Networks["default"].Name epSettings := createEndpointSettings(project, service, serviceIndex, networkKey, links, useNetworkAliases)
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
mobyNetworkName: epSettings,
},
}
// From the Engine API docs:
// > Supported standard values are: bridge, host, none, and container:<name|id>.
// > Any other value is taken as a custom network's name to which this container should connect to.
return container.NetworkMode(mobyNetworkName), networkConfig
} }
func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy { func getRestartPolicy(service types.ServiceConfig) container.RestartPolicy {
@ -1002,14 +1039,6 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions {
} }
} }
func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
aliases := []string{s.Name}
if c != nil {
aliases = append(aliases, c.Aliases...)
}
return aliases
}
func (s *composeService) ensureNetwork(ctx context.Context, n *types.NetworkConfig) error { func (s *composeService) ensureNetwork(ctx context.Context, n *types.NetworkConfig) error {
if n.External.External { if n.External.External {
return s.resolveExternalNetwork(ctx, n) return s.resolveExternalNetwork(ctx, n)

View File

@ -22,6 +22,8 @@ import (
"sort" "sort"
"testing" "testing"
"gotest.tools/v3/assert/cmp"
"github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/api"
composetypes "github.com/compose-spec/compose-go/types" composetypes "github.com/compose-spec/compose-go/types"
@ -203,7 +205,7 @@ func TestBuildContainerMountOptions(t *testing.T) {
assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine") assert.Equal(t, mounts[2].Target, "\\\\.\\pipe\\docker_engine")
} }
func TestGetDefaultNetworkMode(t *testing.T) { func TestDefaultNetworkSettings(t *testing.T) {
t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) { t.Run("returns the network with the highest priority when service has multiple networks", func(t *testing.T) {
service := composetypes.ServiceConfig{ service := composetypes.ServiceConfig{
Name: "myService", Name: "myService",
@ -231,7 +233,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
}), }),
} }
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_myNetwork2") networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
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 default network when service has no networks", func(t *testing.T) { t.Run("returns default network when service has no networks", func(t *testing.T) {
@ -256,7 +261,10 @@ func TestGetDefaultNetworkMode(t *testing.T) {
}), }),
} }
assert.Equal(t, getDefaultNetworkMode(&project, service), "myProject_default") networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "myProject_default")
assert.Check(t, cmp.Len(networkConfig.EndpointsConfig, 1))
assert.Check(t, cmp.Contains(networkConfig.EndpointsConfig, "myProject_default"))
}) })
t.Run("returns none if project has no networks", func(t *testing.T) { t.Run("returns none if project has no networks", func(t *testing.T) {
@ -270,6 +278,28 @@ func TestGetDefaultNetworkMode(t *testing.T) {
}, },
} }
assert.Equal(t, getDefaultNetworkMode(&project, service), "none") networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "none")
assert.Check(t, cmp.Nil(networkConfig))
})
t.Run("returns defined network mode if explicitly set", func(t *testing.T) {
service := composetypes.ServiceConfig{
Name: "myService",
NetworkMode: "host",
}
project := composetypes.Project{
Name: "myProject",
Services: []composetypes.ServiceConfig{service},
Networks: composetypes.Networks(map[string]composetypes.NetworkConfig{
"default": {
Name: "myProject_default",
},
}),
}
networkMode, networkConfig := defaultNetworkSettings(&project, service, 1, nil, true)
assert.Equal(t, string(networkMode), "host")
assert.Check(t, cmp.Nil(networkConfig))
}) })
} }

View File

@ -99,8 +99,14 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
return "", err return "", err
} }
} }
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts := createOptions{
opts.AutoRemove, opts.UseNetworkAliases, opts.Interactive) AutoRemove: opts.AutoRemove,
AttachStdin: opts.Interactive,
UseNetworkAliases: opts.UseNetworkAliases,
Labels: mergeLabels(service.Labels, service.CustomLabels),
}
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, createOpts)
if err != nil { if err != nil {
return "", err return "", err
} }