diff --git a/formatter/logs.go b/formatter/logs.go index b70b9f458..c3542bcd1 100644 --- a/formatter/logs.go +++ b/formatter/logs.go @@ -48,7 +48,7 @@ func (l *logConsumer) Log(service, container, message string) { l.colors[service] = cf l.computeWidth() } - prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", service) + prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container) for _, line := range strings.Split(message, "\n") { buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line)) diff --git a/local/compose/attach.go b/local/compose/attach.go index 3866cf9af..9cccf3c54 100644 --- a/local/compose/attach.go +++ b/local/compose/attach.go @@ -45,7 +45,7 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con var names []string for _, c := range containers { - names = append(names, getContainerName(c)) + names = append(names, getCanonicalContainerName(c)) } fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) @@ -61,7 +61,7 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, con func (s *composeService) attachContainer(ctx context.Context, container moby.Container, consumer compose.LogConsumer, project *types.Project) error { serviceName := container.Labels[serviceLabel] - w := getWriter(serviceName, container.ID, consumer) + w := getWriter(serviceName, getCanonicalContainerName(container), consumer) service, err := project.GetService(serviceName) if err != nil { diff --git a/local/compose/compose.go b/local/compose/compose.go index 402f3f0e0..88cb6fc40 100644 --- a/local/compose/compose.go +++ b/local/compose/compose.go @@ -25,10 +25,11 @@ import ( "github.com/docker/compose-cli/api/compose" "github.com/compose-spec/compose-go/types" - errdefs2 "github.com/docker/compose-cli/errdefs" moby "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/sanathkr/go-yaml" + + errdefs2 "github.com/docker/compose-cli/errdefs" ) // NewComposeService create a local implementation of the compose.Service API @@ -44,7 +45,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options return errdefs2.ErrNotImplemented } -func getContainerName(c moby.Container) string { +func getCanonicalContainerName(c moby.Container) string { // Names return container canonical name /foo + link aliases /linked_by/foo for _, name := range c.Names { if strings.LastIndex(name, "/") == 0 { diff --git a/local/compose/containers.go b/local/compose/containers.go index 4159045c4..300700e23 100644 --- a/local/compose/containers.go +++ b/local/compose/containers.go @@ -66,7 +66,7 @@ func (containers Containers) split(predicate containerPredicate) (Containers, Co func (containers Containers) names() []string { var names []string for _, c := range containers { - names = append(names, getContainerName(c)) + names = append(names, getCanonicalContainerName(c)) } return names } diff --git a/local/compose/convergence.go b/local/compose/convergence.go index 4721a8ab0..0402cc5f8 100644 --- a/local/compose/convergence.go +++ b/local/compose/convergence.go @@ -35,22 +35,23 @@ import ( const ( extLifecycle = "x-lifecycle" forceRecreate = "force_recreate" + + doubledContainerNameWarning = "WARNING: The %q service is using the custom container name %q. " + + "Docker requires each container to have a unique name. " + + "Remove the custom name to scale the service.\n" ) -func (s *composeService) ensureService(ctx context.Context, observedState Containers, project *types.Project, service types.ServiceConfig) error { - scale := getScale(service) - actual := observedState.filter(isService(service.Name)) - +func (s *composeService) ensureScale(ctx context.Context, actual []moby.Container, scale int, project *types.Project, service types.ServiceConfig) (*errgroup.Group, []moby.Container, error) { eg, _ := errgroup.WithContext(ctx) if len(actual) < scale { next, err := nextContainerNumber(actual) if err != nil { - return err + return nil, actual, err } missing := scale - len(actual) for i := 0; i < missing; i++ { number := next + i - name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number) + name := getContainerName(project.Name, service, number) eg.Go(func() error { return s.createContainer(ctx, project, service, name, number, false) }) @@ -70,6 +71,21 @@ func (s *composeService) ensureService(ctx context.Context, observedState Contai } actual = actual[:scale] } + return eg, actual, nil +} + +func (s *composeService) ensureService(ctx context.Context, observedState Containers, project *types.Project, service types.ServiceConfig) 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) + if err != nil { + return err + } expected, err := jsonHash(service) if err != nil { @@ -78,7 +94,7 @@ func (s *composeService) ensureService(ctx context.Context, observedState Contai for _, container := range actual { container := container - name := getContainerName(container) + name := getCanonicalContainerName(container) diverged := container.Labels[configHashLabel] != expected if diverged || service.Extensions[extLifecycle] == forceRecreate { @@ -104,6 +120,14 @@ func (s *composeService) ensureService(ctx context.Context, observedState Contai return eg.Wait() } +func getContainerName(projectName string, service types.ServiceConfig, number int) string { + name := fmt.Sprintf("%s_%s_%d", projectName, service.Name, number) + if service.ContainerName != "" { + name = service.ContainerName + } + return name +} + func (s *composeService) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error { eg, _ := errgroup.WithContext(ctx) for dep, config := range service.DependsOn { @@ -143,14 +167,22 @@ func nextContainerNumber(containers []moby.Container) (int, error) { } -func getScale(config types.ServiceConfig) int { +func getScale(config types.ServiceConfig) (int, error) { + scale := 1 + var err error if config.Deploy != nil && config.Deploy.Replicas != nil { - return int(*config.Deploy.Replicas) + scale = int(*config.Deploy.Replicas) } if config.Scale != 0 { - return config.Scale + scale = config.Scale } - return 1 + if scale > 1 && config.ContainerName != "" { + scale = -1 + err = fmt.Errorf(doubledContainerNameWarning, + config.Name, + config.ContainerName) + } + return scale, err } func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, autoRemove bool) error { @@ -166,12 +198,12 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container) error { w := progress.ContextWriter(ctx) - w.Event(progress.NewEvent(getContainerName(container), progress.Working, "Recreate")) + w.Event(progress.NewEvent(getCanonicalContainerName(container), progress.Working, "Recreate")) err := s.apiClient.ContainerStop(ctx, container.ID, nil) if err != nil { return err } - name := getContainerName(container) + name := getCanonicalContainerName(container) tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name) err = s.apiClient.ContainerRename(ctx, container.ID, tmpName) if err != nil { @@ -189,7 +221,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P if err != nil { return err } - w.Event(progress.NewEvent(getContainerName(container), progress.Done, "Recreated")) + w.Event(progress.NewEvent(getCanonicalContainerName(container), progress.Done, "Recreated")) setDependentLifecycle(project, service.Name, forceRecreate) return nil } @@ -209,12 +241,12 @@ func setDependentLifecycle(project *types.Project, service string, strategy stri func (s *composeService) restartContainer(ctx context.Context, container moby.Container) error { w := progress.ContextWriter(ctx) - w.Event(progress.NewEvent(getContainerName(container), progress.Working, "Restart")) + w.Event(progress.NewEvent(getCanonicalContainerName(container), progress.Working, "Restart")) err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) if err != nil { return err } - w.Event(progress.NewEvent(getContainerName(container), progress.Done, "Restarted")) + w.Event(progress.NewEvent(getCanonicalContainerName(container), progress.Done, "Restarted")) return nil } @@ -229,8 +261,8 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types } id := created.ID for netName := range service.Networks { - network := project.Networks[netName] - err = s.connectContainerToNetwork(ctx, id, service.Name, network.Name) + netwrk := project.Networks[netName] + err = s.connectContainerToNetwork(ctx, id, netwrk.Name, service.Name, getContainerName(project.Name, service, number)) if err != nil { return err } @@ -238,9 +270,9 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types return nil } -func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error { - err := s.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{ - Aliases: []string{service}, +func (s *composeService) connectContainerToNetwork(ctx context.Context, id string, netwrk string, aliases ...string) error { + err := s.apiClient.NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{ + Aliases: aliases, }) if err != nil { return err @@ -300,10 +332,10 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec } eg.Go(func() error { w := progress.ContextWriter(ctx) - w.Event(progress.StartingEvent(getContainerName(container))) + w.Event(progress.StartingEvent(getCanonicalContainerName(container))) err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{}) if err == nil { - w.Event(progress.StartedEvent(getContainerName(container))) + w.Event(progress.StartedEvent(getCanonicalContainerName(container))) } return err }) diff --git a/local/compose/convergence_test.go b/local/compose/convergence_test.go new file mode 100644 index 000000000..3a5009a06 --- /dev/null +++ b/local/compose/convergence_test.go @@ -0,0 +1,55 @@ +/* + 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 ( + "fmt" + "testing" + + "github.com/compose-spec/compose-go/types" + "gotest.tools/assert" +) + +func TestContainerName(t *testing.T) { + var replicas uint64 = 1 + s := types.ServiceConfig{ + Name: "testservicename", + ContainerName: "testcontainername", + Scale: 1, + Deploy: &types.DeployConfig{}, + } + ret, err := getScale(s) + assert.NilError(t, err) + assert.Equal(t, ret, s.Scale) + + s.Scale = 0 + s.Deploy.Replicas = &replicas + ret, err = getScale(s) + assert.NilError(t, err) + assert.Equal(t, ret, int(*s.Deploy.Replicas)) + + s.Deploy.Replicas = nil + s.Scale = 2 + _, err = getScale(s) + assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName)) + + replicas = 2 + s.Deploy.Replicas = &replicas + s.Scale = 0 + _, err = getScale(s) + assert.Error(t, err, fmt.Sprintf(doubledContainerNameWarning, s.Name, s.ContainerName)) +} diff --git a/local/compose/create.go b/local/compose/create.go index 8855b2efc..6e445ce73 100644 --- a/local/compose/create.go +++ b/local/compose/create.go @@ -213,7 +213,7 @@ func getCreateOptions(p *types.Project, s types.ServiceConfig, number int, inher Resources: resources, } - networkConfig := buildDefaultNetworkConfig(s, networkMode) + networkConfig := buildDefaultNetworkConfig(s, networkMode, getContainerName(p.Name, s, number)) return &containerConfig, &hostConfig, networkConfig, nil } @@ -359,11 +359,11 @@ func buildTmpfsOptions(tmpfs *types.ServiceVolumeTmpfs) *mount.TmpfsOptions { } } -func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode) *network.NetworkingConfig { +func buildDefaultNetworkConfig(s types.ServiceConfig, networkMode container.NetworkMode, containerName string) *network.NetworkingConfig { config := map[string]*network.EndpointSettings{} net := string(networkMode) config[net] = &network.EndpointSettings{ - Aliases: getAliases(s, s.Networks[net]), + Aliases: append(getAliases(s, s.Networks[net]), containerName), } return &network.NetworkingConfig{ diff --git a/local/compose/down.go b/local/compose/down.go index 2d2d203dc..bd699ae6b 100644 --- a/local/compose/down.go +++ b/local/compose/down.go @@ -94,7 +94,7 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer for _, container := range containers { toDelete := container eg.Go(func() error { - eventName := "Container " + getContainerName(toDelete) + eventName := "Container " + getCanonicalContainerName(toDelete) w.Event(progress.StoppingEvent(eventName)) err := s.apiClient.ContainerStop(ctx, toDelete.ID, nil) if err != nil { diff --git a/local/compose/ps.go b/local/compose/ps.go index 7c6ca1b5e..9dfcdd7eb 100644 --- a/local/compose/ps.go +++ b/local/compose/ps.go @@ -21,9 +21,10 @@ import ( "fmt" "sort" - "github.com/docker/compose-cli/api/compose" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" + + "github.com/docker/compose-cli/api/compose" ) func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) { @@ -54,7 +55,7 @@ func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose. summary = append(summary, compose.ContainerSummary{ ID: c.ID, - Name: getContainerName(c), + Name: getCanonicalContainerName(c), Project: c.Labels[projectLabel], Service: c.Labels[serviceLabel], State: c.State,