Repository: docker/compose-cli
Pull request #1262 (2021-02-09 13:23:47)

Add service based network_mode
**What I did**
Add service based network_mode

**Related issue**
Resolves https://github.com/docker/compose-cli/issues/1011

Depends on https://github.com/compose-spec/compose-go/pull/103

ulyssessouza: @ndeloof Looks like you commented in the wrong issue 😸 

Patch:
diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go
index 1b633563..5bb63cd9 100644
--- a/cli/cmd/compose/compose.go
+++ b/cli/cmd/compose/compose.go
@@ -69,7 +69,7 @@ func (o *projectOptions) toProject(services []string) (*types.Project, error) {
 	}
 
 	if len(services) != 0 {
-		s, err := project.GetServices(services)
+		s, err := project.GetServices(services...)
 		if err != nil {
 			return nil, err
 		}
diff --git a/go.mod b/go.mod
index 6926a55c..808e446e 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/awslabs/goformation/v4 v4.15.6
 	github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
 	github.com/cnabio/cnab-to-oci v0.3.1-beta1
-	github.com/compose-spec/compose-go v0.0.0-20210217144939-9f2c61fe6b14
+	github.com/compose-spec/compose-go v0.0.0-20210218184709-a75bbdcff7f3
 	github.com/containerd/console v1.0.1
 	github.com/containerd/containerd v1.4.3
 	github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a // indirect
diff --git a/go.sum b/go.sum
index 1aff162b..2e413434 100644
--- a/go.sum
+++ b/go.sum
@@ -302,8 +302,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
-github.com/compose-spec/compose-go v0.0.0-20210217144939-9f2c61fe6b14 h1:ezR3VeA9GLPRMXYC0JzuHOM6c9yHMbc6EHaZy6lEKRA=
-github.com/compose-spec/compose-go v0.0.0-20210217144939-9f2c61fe6b14/go.mod h1:flNthwF3kg+JioxATZWSsuuA2N3zGGwggDNNGoE9PHA=
+github.com/compose-spec/compose-go v0.0.0-20210218184709-a75bbdcff7f3 h1:cGJa3EMDcclDU21e/CVQJnDf3ZjnB6HN9TvxkFHpGq8=
+github.com/compose-spec/compose-go v0.0.0-20210218184709-a75bbdcff7f3/go.mod h1:flNthwF3kg+JioxATZWSsuuA2N3zGGwggDNNGoE9PHA=
 github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
 github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
 github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340 h1:9atoWyI9RtXFwf7UDbme/6M8Ud0rFrx+Q3ZWgSnsxtw=
diff --git a/local/compose/convergence.go b/local/compose/convergence.go
index 5ba38940..97fa40fc 100644
--- a/local/compose/convergence.go
+++ b/local/compose/convergence.go
@@ -42,7 +42,17 @@ const (
 		"Remove the custom name to scale the service.\n"
 )
 
-func (s *composeService) ensureScale(ctx context.Context, actual []moby.Container, scale int, project *types.Project, service types.ServiceConfig) (*errgroup.Group, []moby.Container, error) {
+func (s *composeService) ensureScale(ctx context.Context, project *types.Project, service types.ServiceConfig) (*errgroup.Group, []moby.Container, error) {
+	cState, err := GetContextContainerState(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	observedState := cState.GetContainers()
+	actual := observedState.filter(isService(service.Name))
+	scale, err := getScale(service)
+	if err != nil {
+		return nil, nil, err
+	}
 	eg, _ := errgroup.WithContext(ctx)
 	if len(actual) < scale {
 		next, err := nextContainerNumber(actual)
@@ -75,15 +85,8 @@ func (s *composeService) ensureScale(ctx context.Context, actual []moby.Containe
 	return eg, actual, nil
 }
 
-func (s *composeService) ensureService(ctx context.Context, observedState Containers, project *types.Project, service types.ServiceConfig, recreate string) error {
-	actual := observedState.filter(isService(service.Name))
-
-	scale, err := getScale(service)
-	if err != nil {
-		return err
-	}
-
-	eg, actual, err := s.ensureScale(ctx, actual, scale, project, service)
+func (s *composeService) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string) error {
+	eg, actual, err := s.ensureScale(ctx, project, service)
 	if err != nil {
 		return err
 	}
@@ -260,7 +263,12 @@ func (s *composeService) restartContainer(ctx context.Context, container moby.Co
 	return nil
 }
 
-func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container, autoRemove bool) error {
+func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container,
+	autoRemove bool) error {
+	cState, err := GetContextContainerState(ctx)
+	if err != nil {
+		return err
+	}
 	containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, container, autoRemove)
 	if err != nil {
 		return err
@@ -269,10 +277,14 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
 	if err != nil {
 		return err
 	}
-	id := created.ID
+	createdContainer := moby.Container{
+		ID:     created.ID,
+		Labels: containerConfig.Labels,
+	}
+	cState.Add(createdContainer)
 	for netName := range service.Networks {
 		netwrk := project.Networks[netName]
-		err = s.connectContainerToNetwork(ctx, id, netwrk.Name, service.Name, getContainerName(project.Name, service, number))
+		err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, service.Name, getContainerName(project.Name, service, number))
 		if err != nil {
 			return err
 		}
diff --git a/local/compose/create.go b/local/compose/create.go
index 5b0e121b..13cd307a 100644
--- a/local/compose/create.go
+++ b/local/compose/create.go
@@ -70,6 +70,8 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
 	if err != nil {
 		return err
 	}
+	containerState := NewContainersState(observedState)
+	ctx = context.WithValue(ctx, ContainersKey{}, containerState)
 
 	allServices := project.AllServices()
 	allServiceNames := []string{}
@@ -92,8 +94,10 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
 		}
 	}
 
+	prepareNetworkMode(project)
+
 	return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		return s.ensureService(c, observedState, project, service, opts.Recreate)
+		return s.ensureService(c, project, service, opts.Recreate)
 	})
 }
 
@@ -129,6 +133,27 @@ func prepareNetworks(project *types.Project) {
 	}
 }
 
+func prepareNetworkMode(p *types.Project) {
+outLoop:
+	for i := range p.Services {
+		dependency := getDependentServiceByNetwork(p.Services[i].NetworkMode)
+		if dependency == "" {
+			continue
+		}
+		if p.Services[i].DependsOn == nil {
+			p.Services[i].DependsOn = make(types.DependsOnConfig)
+		}
+		for _, service := range p.Services {
+			if service.Name == dependency {
+				p.Services[i].DependsOn[service.Name] = types.ServiceDependency{
+					Condition: types.ServiceConditionStarted,
+				}
+				continue outLoop
+			}
+		}
+	}
+}
+
 func (s *composeService) ensureNetworks(ctx context.Context, networks types.Networks) error {
 	for _, network := range networks {
 		err := s.ensureNetwork(ctx, network)
@@ -235,7 +260,11 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 	portBindings := buildContainerPortBindingOptions(service)
 
 	resources := getDeployResources(service)
-	networkMode := getNetworkMode(p, service)
+
+	networkMode, err := getNetworkMode(ctx, p, service)
+	if err != nil {
+		return nil, nil, nil, err
+	}
 	hostConfig := container.HostConfig{
 		AutoRemove:     autoRemove,
 		Binds:          binds,
@@ -335,6 +364,14 @@ func getVolumesFrom(project *types.Project, volumesFrom []string) ([]string, []s
 
 }
 
+func getDependentServiceByNetwork(networkMode string) string {
+	baseService := ""
+	if strings.HasPrefix(networkMode, types.NetworkModeServicePrefix) {
+		return networkMode[len(types.NetworkModeServicePrefix):]
+	}
+	return baseService
+}
+
 func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Project, service types.ServiceConfig,
 	inherit *moby.Container) (map[string]struct{}, []string, []mount.Mount, error) {
 	var mounts = []mount.Mount{}
@@ -602,32 +639,42 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
 	return aliases
 }
 
-func getNetworkMode(p *types.Project, service types.ServiceConfig) container.NetworkMode {
+func getNetworkMode(ctx context.Context, p *types.Project, service types.ServiceConfig) (container.NetworkMode, error) {
+	cState, err := GetContextContainerState(ctx)
+	if err != nil {
+		return container.NetworkMode("none"), nil
+	}
+	observedState := cState.GetContainers()
+
 	mode := service.NetworkMode
 	if mode == "" {
 		if len(p.Networks) > 0 {
 			for name := range getNetworksForService(service) {
-				return container.NetworkMode(p.Networks[name].Name)
+				return container.NetworkMode(p.Networks[name].Name), nil
 			}
 		}
-		return container.NetworkMode("none")
+		return container.NetworkMode("none"), nil
 	}
-
-	// FIXME incomplete implementation
-	if strings.HasPrefix(mode, "service:") {
-		panic("Not yet implemented")
-	}
-	if strings.HasPrefix(mode, "container:") {
-		panic("Not yet implemented")
+	depServiceNetworkMode := getDependentServiceByNetwork(service.NetworkMode)
+	if depServiceNetworkMode != "" {
+		depServiceContainers := observedState.filter(isService(depServiceNetworkMode))
+		if len(depServiceContainers) > 0 {
+			return container.NetworkMode(types.NetworkModeContainerPrefix + depServiceContainers[0].ID), nil
+		}
+		return container.NetworkMode("none"),
+			fmt.Errorf(`no containers started for network_mode %q in service %q -> %v`,
+				mode, service.Name, observedState)
 	}
-
-	return container.NetworkMode(mode)
+	return container.NetworkMode(mode), nil
 }
 
 func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
 	if len(s.Networks) > 0 {
 		return s.Networks
 	}
+	if s.NetworkMode != "" {
+		return nil
+	}
 	return map[string]*types.ServiceNetworkConfig{"default": nil}
 }
 
diff --git a/local/compose/run.go b/local/compose/run.go
index 9f093d4d..d29f07f1 100644
--- a/local/compose/run.go
+++ b/local/compose/run.go
@@ -30,6 +30,16 @@ import (
 )
 
 func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.Project, opts compose.RunOptions) (int, error) {
+	observedState, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{
+		Filters: filters.NewArgs(projectFilter(project.Name)),
+		All:     true,
+	})
+	if err != nil {
+		return 0, err
+	}
+	containerState := NewContainersState(observedState)
+	ctx = context.WithValue(ctx, ContainersKey{}, containerState)
+
 	service, err := project.GetService(opts.Service)
 	if err != nil {
 		return 0, err
diff --git a/local/compose/status.go b/local/compose/status.go
new file mode 100644
index 00000000..854b1918
--- /dev/null
+++ b/local/compose/status.go
@@ -0,0 +1,102 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"context"
+
+	"github.com/docker/docker/api/types"
+	"github.com/pkg/errors"
+)
+
+// ContainersKey is the context key to access context value os a ContainersStatus
+type ContainersKey struct{}
+
+// ContainersState state management interface
+type ContainersState interface {
+	Get(string) *types.Container
+	GetContainers() Containers
+	Add(c types.Container)
+	AddAll(cs Containers)
+	Remove(string) types.Container
+}
+
+// NewContainersState creates a new container state manager
+func NewContainersState(cs Containers) ContainersState {
+	s := containersState{
+		observedContainers: &cs,
+	}
+	return &s
+}
+
+// ContainersStatus works as a collection container for the observed containers
+type containersState struct {
+	observedContainers *Containers
+}
+
+func (s *containersState) AddAll(cs Containers) {
+	for _, c := range cs {
+		lValue := append(*s.observedContainers, c)
+		s.observedContainers = &lValue
+	}
+}
+
+func (s *containersState) Add(c types.Container) {
+	if s.Get(c.ID) == nil {
+		lValue := append(*s.observedContainers, c)
+		s.observedContainers = &lValue
+	}
+}
+
+func (s *containersState) Remove(id string) types.Container {
+	var c types.Container
+	var newObserved Containers
+	for _, o := range *s.observedContainers {
+		if o.ID != id {
+			c = o
+			continue
+		}
+		newObserved = append(newObserved, o)
+	}
+	s.observedContainers = &newObserved
+	return c
+}
+
+func (s *containersState) Get(id string) *types.Container {
+	for _, o := range *s.observedContainers {
+		if id == o.ID {
+			return &o
+		}
+	}
+	return nil
+}
+
+func (s *containersState) GetContainers() Containers {
+	if s.observedContainers != nil && *s.observedContainers != nil {
+		return *s.observedContainers
+	}
+	return make(Containers, 0)
+}
+
+// GetContextContainerState gets the container state manager
+func GetContextContainerState(ctx context.Context) (ContainersState, error) {
+	cState, ok := ctx.Value(ContainersKey{}).(*containersState)
+	if !ok {
+		return nil, errors.New("containers' containersState not available in context")
+	}
+	return cState, nil
+}


Test patch:
diff --git a/cli/cmd/compose/compose_test.go b/cli/cmd/compose/compose_test.go
index dfa68cf4..b1940442 100644
--- a/cli/cmd/compose/compose_test.go
+++ b/cli/cmd/compose/compose_test.go
@@ -32,7 +32,7 @@ func TestFilterServices(t *testing.T) {
 			},
 			{
 				Name:        "bar",
-				NetworkMode: "service:zot",
+				NetworkMode: types.NetworkModeServicePrefix + "zot",
 			},
 			{
 				Name: "zot",
diff --git a/local/e2e/compose/fixtures/network-test/compose.yaml b/local/e2e/compose/fixtures/network-test/compose.yaml
index e3435fe3..bb49f318 100644
--- a/local/e2e/compose/fixtures/network-test/compose.yaml
+++ b/local/e2e/compose/fixtures/network-test/compose.yaml
@@ -1,4 +1,9 @@
 services:
+  mydb:
+    image: mysql
+    network_mode: "service:db"
+    environment:
+      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
   db:
     image: gtardif/sentences-db
     networks: