Skip to content

Commit 10b290e

Browse files
committed
up: fix race condition on network connect
Engine API only allows at most one network to be connected as part of the ContainerCreate API request. Compose will pick the highest priority network. Afterwards, the remaining networks (if any) are connected before the container is actually started. The big change here is that, previously, the highest-priority network was connected in the create, and then disconnected and immediately reconnected along with all the others. This was racy because evidently connecting the container to the network as part of the create isn't synchronous, so sometimes when Compose tried to disconnect it, the API would return an error like: ``` container <id> is not connected to the network <network> ``` To avoid needing to disconnect and immediately reconnect, the network config logic has been refactored to ensure that it sets up the network config correctly the first time. Signed-off-by: Milas Bowman <[email protected]>
1 parent 3906a7a commit 10b290e

File tree

4 files changed

+170
-156
lines changed

4 files changed

+170
-156
lines changed

pkg/compose/convergence.go

Lines changed: 31 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import (
2929
"github.com/containerd/containerd/platforms"
3030
moby "github.com/docker/docker/api/types"
3131
containerType "github.com/docker/docker/api/types/container"
32-
"github.com/docker/docker/api/types/network"
3332
specs "github.com/opencontainers/image-spec/specs-go/v1"
3433
"github.com/pkg/errors"
3534
"github.com/sirupsen/logrus"
@@ -233,7 +232,13 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
233232
name := getContainerName(project.Name, service, number)
234233
i := i
235234
eg.Go(func() error {
236-
container, err := c.service.createContainer(ctx, project, service, name, number, false, true, false)
235+
opts := createOptions{
236+
AutoRemove: false,
237+
AttachStdin: false,
238+
UseNetworkAliases: true,
239+
Labels: mergeLabels(service.Labels, service.CustomLabels),
240+
}
241+
container, err := c.service.createContainer(ctx, project, service, name, number, opts)
237242
updated[actual+i] = container
238243
return err
239244
})
@@ -399,12 +404,11 @@ func getScale(config types.ServiceConfig) (int, error) {
399404
}
400405

401406
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
402-
name string, number int, autoRemove bool, useNetworkAliases bool, attachStdin bool) (container moby.Container, err error) {
407+
name string, number int, opts createOptions) (container moby.Container, err error) {
403408
w := progress.ContextWriter(ctx)
404409
eventName := "Container " + name
405410
w.Event(progress.CreatingEvent(eventName))
406-
container, err = s.createMobyContainer(ctx, project, service, name, number, nil,
407-
autoRemove, useNetworkAliases, attachStdin, w, mergeLabels(service.Labels, service.CustomLabels))
411+
container, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts, w)
408412
if err != nil {
409413
return
410414
}
@@ -429,9 +433,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
429433
}
430434
name := getContainerName(project.Name, service, number)
431435
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
432-
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited,
433-
false, true, false, w,
434-
mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID))
436+
opts := createOptions{
437+
AutoRemove: false,
438+
AttachStdin: false,
439+
UseNetworkAliases: true,
440+
Labels: mergeLabels(service.Labels, service.CustomLabels).Add(api.ContainerReplaceLabel, replaced.ID),
441+
}
442+
created, err = s.createMobyContainer(ctx, project, service, tmpName, number, inherited, opts, w)
435443
if err != nil {
436444
return created, err
437445
}
@@ -484,19 +492,18 @@ func (s *composeService) startContainer(ctx context.Context, container moby.Cont
484492
return nil
485493
}
486494

487-
func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyclo
495+
func (s *composeService) createMobyContainer(ctx context.Context,
488496
project *types.Project,
489497
service types.ServiceConfig,
490498
name string,
491499
number int,
492500
inherit *moby.Container,
493-
autoRemove, useNetworkAliases, attachStdin bool,
501+
opts createOptions,
494502
w progress.Writer,
495-
labels types.Labels,
496503
) (moby.Container, error) {
497504
var created moby.Container
498-
containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit,
499-
autoRemove, attachStdin, labels)
505+
cfgs, err := s.getCreateConfigs(ctx, project, service, number, inherit, opts)
506+
500507
if err != nil {
501508
return created, err
502509
}
@@ -514,18 +521,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyc
514521
plat = &p
515522
}
516523

517-
links, err := s.getLinks(ctx, project.Name, service, number)
518-
if err != nil {
519-
return created, err
520-
}
521-
if networkingConfig != nil {
522-
for k, s := range networkingConfig.EndpointsConfig {
523-
s.Links = links
524-
networkingConfig.EndpointsConfig[k] = s
525-
}
526-
}
527-
528-
response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
524+
response, err := s.apiClient().ContainerCreate(ctx, cfgs.Container, cfgs.Host, cfgs.Network, plat, name)
529525
if err != nil {
530526
return created, err
531527
}
@@ -548,29 +544,19 @@ func (s *composeService) createMobyContainer(ctx context.Context, //nolint:gocyc
548544
Networks: inspectedContainer.NetworkSettings.Networks,
549545
},
550546
}
551-
for _, netName := range service.NetworksByPriority() {
552-
netwrk := project.Networks[netName]
553-
cfg := service.Networks[netName]
554-
aliases := []string{getContainerName(project.Name, service, number)}
555-
if useNetworkAliases {
556-
aliases = append(aliases, service.Name)
557-
if cfg != nil {
558-
aliases = append(aliases, cfg.Aliases...)
559-
}
560-
}
561-
if val, ok := created.NetworkSettings.Networks[netwrk.Name]; ok {
562-
if shortIDAliasExists(created.ID, val.Aliases...) {
563-
continue
564-
}
565-
err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
566-
if err != nil {
547+
548+
// the highest-priority network is the primary and is included in the ContainerCreate API
549+
// call via container.NetworkMode & network.NetworkingConfig
550+
// any remaining networks are connected one-by-one here after creation (but before start)
551+
serviceNetworks := service.NetworksByPriority()
552+
if len(serviceNetworks) > 1 {
553+
for _, networkKey := range serviceNetworks[1:] {
554+
mobyNetworkName := project.Networks[networkKey].Name
555+
epSettings := createEndpointSettings(project, service, number, networkKey, cfgs.Links, opts.UseNetworkAliases)
556+
if err := s.apiClient().NetworkConnect(ctx, mobyNetworkName, created.ID, epSettings); err != nil {
567557
return created, err
568558
}
569559
}
570-
err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, cfg, links, aliases...)
571-
if err != nil {
572-
return created, err
573-
}
574560
}
575561

576562
err = s.injectSecrets(ctx, project, service, created.ID)
@@ -635,43 +621,6 @@ func (s *composeService) getLinks(ctx context.Context, projectName string, servi
635621
return links, nil
636622
}
637623

638-
func shortIDAliasExists(containerID string, aliases ...string) bool {
639-
for _, alias := range aliases {
640-
if alias == containerID[:12] {
641-
return true
642-
}
643-
}
644-
return false
645-
}
646-
647-
func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, netwrk string, cfg *types.ServiceNetworkConfig, links []string, aliases ...string) error {
648-
var (
649-
ipv4Address string
650-
ipv6Address string
651-
ipam *network.EndpointIPAMConfig
652-
)
653-
if cfg != nil {
654-
ipv4Address = cfg.Ipv4Address
655-
ipv6Address = cfg.Ipv6Address
656-
ipam = &network.EndpointIPAMConfig{
657-
IPv4Address: ipv4Address,
658-
IPv6Address: ipv6Address,
659-
LinkLocalIPs: cfg.LinkLocalIPs,
660-
}
661-
}
662-
err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
663-
Aliases: aliases,
664-
IPAddress: ipv4Address,
665-
GlobalIPv6Address: ipv6Address,
666-
Links: links,
667-
IPAMConfig: ipam,
668-
})
669-
if err != nil {
670-
return err
671-
}
672-
return nil
673-
}
674-
675624
func (s *composeService) isServiceHealthy(ctx context.Context, containers Containers, fallbackRunning bool) (bool, error) {
676625
for _, c := range containers {
677626
container, err := s.apiClient().ContainerInspect(ctx, c.ID)

0 commit comments

Comments
 (0)