Quellcode durchsuchen

Merge pull request #1071 from docker/local_ecs_moby

use compose-in-go code to implement local ECS simulation context
Guillaume Tardif vor 4 Jahren
Ursprung
Commit
a17e397df3

+ 6 - 6
aci/compose.go

@@ -64,7 +64,7 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project,
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error {
+func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	logrus.Debugf("Up on project with name %q", project.Name)
 
 	if err := autocreateFileshares(ctx, project); err != nil {
@@ -102,14 +102,14 @@ func (cs aciComposeService) warnKeepVolumeOnDown(ctx context.Context, projectNam
 	return nil
 }
 
-func (cs *aciComposeService) Down(ctx context.Context, project string) error {
-	logrus.Debugf("Down on project with name %q", project)
+func (cs *aciComposeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
+	logrus.Debugf("Down on projectName with name %q", projectName)
 
-	if err := cs.warnKeepVolumeOnDown(ctx, project); err != nil {
+	if err := cs.warnKeepVolumeOnDown(ctx, projectName); err != nil {
 		return err
 	}
 
-	cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
+	cg, err := deleteACIContainerGroup(ctx, cs.ctx, projectName)
 	if err != nil {
 		return err
 	}
@@ -198,6 +198,6 @@ func (cs *aciComposeService) Logs(ctx context.Context, projectName string, consu
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
+func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }

+ 3 - 3
api/client/compose.go

@@ -48,11 +48,11 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, cons
 	return errdefs.ErrNotImplemented
 }
 
-func (c *composeService) Up(context.Context, *types.Project, bool) error {
+func (c *composeService) Up(context.Context, *types.Project, compose.UpOptions) error {
 	return errdefs.ErrNotImplemented
 }
 
-func (c *composeService) Down(context.Context, string) error {
+func (c *composeService) Down(context.Context, string, compose.DownOptions) error {
 	return errdefs.ErrNotImplemented
 }
 
@@ -68,6 +68,6 @@ func (c *composeService) List(context.Context, string) ([]compose.Stack, error)
 	return nil, errdefs.ErrNotImplemented
 }
 
-func (c *composeService) Convert(context.Context, *types.Project, string) ([]byte, error) {
+func (c *composeService) Convert(context.Context, *types.Project, compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }

+ 21 - 3
api/compose/api.go

@@ -35,9 +35,9 @@ type Service interface {
 	// Start executes the equivalent to a `compose start`
 	Start(ctx context.Context, project *types.Project, consumer LogConsumer) error
 	// Up executes the equivalent to a `compose up`
-	Up(ctx context.Context, project *types.Project, detach bool) error
+	Up(ctx context.Context, project *types.Project, options UpOptions) error
 	// Down executes the equivalent to a `compose down`
-	Down(ctx context.Context, projectName string) error
+	Down(ctx context.Context, projectName string, options DownOptions) error
 	// Logs executes the equivalent to a `compose logs`
 	Logs(ctx context.Context, projectName string, consumer LogConsumer, options LogOptions) error
 	// Ps executes the equivalent to a `compose ps`
@@ -45,7 +45,25 @@ type Service interface {
 	// List executes the equivalent to a `docker stack ls`
 	List(ctx context.Context, projectName string) ([]Stack, error)
 	// Convert translate compose model into backend's native format
-	Convert(ctx context.Context, project *types.Project, format string) ([]byte, error)
+	Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error)
+}
+
+// UpOptions group options of the Up API
+type UpOptions struct {
+	// Detach will create services and return immediately
+	Detach bool
+}
+
+// DownOptions group options of the Down API
+type DownOptions struct {
+	// RemoveOrphans will cleanup containers that are not declared on the compose model but own the same labels
+	RemoveOrphans bool
+}
+
+// ConvertOptions group options of the Convert API
+type ConvertOptions struct {
+	// Format define the output format used to dump converted application model (json|yaml)
+	Format string
 }
 
 // PortPublisher hold status about published port

+ 5 - 1
cli/cmd/compose/convert.go

@@ -20,6 +20,8 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/spf13/cobra"
 
@@ -61,7 +63,9 @@ func runConvert(ctx context.Context, opts composeOptions) error {
 		return err
 	}
 
-	json, err = c.ComposeService().Convert(ctx, project, opts.Format)
+	json, err = c.ComposeService().Convert(ctx, project, compose.ConvertOptions{
+		Format: opts.Format,
+	})
 	if err != nil {
 		return err
 	}

+ 5 - 1
cli/cmd/compose/down.go

@@ -19,6 +19,8 @@ package compose
 import (
 	"context"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
@@ -52,7 +54,9 @@ func runDown(ctx context.Context, opts composeOptions) error {
 		if err != nil {
 			return "", err
 		}
-		return projectName, c.ComposeService().Down(ctx, projectName)
+		return projectName, c.ComposeService().Down(ctx, projectName, compose.DownOptions{
+			RemoveOrphans: false,
+		})
 	})
 	return err
 }

+ 5 - 3
cli/cmd/compose/up.go

@@ -40,7 +40,7 @@ func upCommand(contextType string) *cobra.Command {
 		Short: "Create and start containers",
 		RunE: func(cmd *cobra.Command, args []string) error {
 			switch contextType {
-			case store.LocalContextType, store.DefaultContextType:
+			case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
 				return runCreateStart(cmd.Context(), opts, args)
 			default:
 				return runUp(cmd.Context(), opts, args)
@@ -68,7 +68,9 @@ func runUp(ctx context.Context, opts composeOptions, services []string) error {
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Up(ctx, project, opts.Detach)
+		return "", c.ComposeService().Up(ctx, project, compose.UpOptions{
+			Detach: opts.Detach,
+		})
 	})
 	return err
 }
@@ -96,7 +98,7 @@ func runCreateStart(ctx context.Context, opts composeOptions, services []string)
 		fmt.Println("Gracefully stopping...")
 		ctx = context.Background()
 		_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-			return "", c.ComposeService().Down(ctx, project.Name)
+			return "", c.ComposeService().Down(ctx, project.Name, compose.DownOptions{})
 		})
 	}
 	return err

+ 4 - 2
ecs/cloudformation.go

@@ -23,6 +23,8 @@ import (
 	"regexp"
 	"strings"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	ecsapi "github.com/aws/aws-sdk-go/service/ecs"
 	"github.com/aws/aws-sdk-go/service/elbv2"
 	cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery"
@@ -37,13 +39,13 @@ import (
 	"github.com/compose-spec/compose-go/types"
 )
 
-func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
+func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
 	template, err := b.convert(ctx, project)
 	if err != nil {
 		return nil, err
 	}
 
-	return marshall(template, format)
+	return marshall(template, options.Format)
 }
 
 func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*cloudformation.Template, error) {

+ 7 - 5
ecs/down.go

@@ -19,11 +19,13 @@ package ecs
 import (
 	"context"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	"github.com/docker/compose-cli/progress"
 )
 
-func (b *ecsAPIService) Down(ctx context.Context, project string) error {
-	resources, err := b.aws.ListStackResources(ctx, project)
+func (b *ecsAPIService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
+	resources, err := b.aws.ListStackResources(ctx, projectName)
 	if err != nil {
 		return err
 	}
@@ -38,16 +40,16 @@ func (b *ecsAPIService) Down(ctx context.Context, project string) error {
 		return err
 	}
 
-	previousEvents, err := b.previousStackEvents(ctx, project)
+	previousEvents, err := b.previousStackEvents(ctx, projectName)
 	if err != nil {
 		return err
 	}
 
-	err = b.aws.DeleteStack(ctx, project)
+	err = b.aws.DeleteStack(ctx, projectName)
 	if err != nil {
 		return err
 	}
-	return b.WaitStackCompletion(ctx, project, stackDelete, previousEvents...)
+	return b.WaitStackCompletion(ctx, projectName, stackDelete, previousEvents...)
 }
 
 func (b *ecsAPIService) previousStackEvents(ctx context.Context, project string) ([]string, error) {

+ 7 - 3
ecs/local/backend.go

@@ -19,6 +19,8 @@ package local
 import (
 	"context"
 
+	local_compose "github.com/docker/compose-cli/local/compose"
+
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/compose"
@@ -38,17 +40,19 @@ func init() {
 }
 
 type ecsLocalSimulation struct {
-	moby *client.Client
+	moby    *client.Client
+	compose compose.Service
 }
 
 func service(ctx context.Context) (backend.Service, error) {
-	apiClient, err := client.NewClientWithOpts(client.FromEnv)
+	apiClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
 	if err != nil {
 		return nil, err
 	}
 
 	return &ecsLocalSimulation{
-		moby: apiClient,
+		moby:    apiClient,
+		compose: local_compose.NewComposeService(apiClient),
 	}, nil
 }
 

+ 41 - 92
ecs/local/compose.go

@@ -17,80 +17,76 @@
 package local
 
 import (
-	"bufio"
-	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
-	"strings"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/compose-spec/compose-go/types"
-	types2 "github.com/docker/docker/api/types"
-	"github.com/docker/docker/api/types/filters"
-	"github.com/pkg/errors"
-	"github.com/sanathkr/go-yaml"
-	"golang.org/x/mod/semver"
-
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/errdefs"
+	"github.com/sanathkr/go-yaml"
 )
 
 func (e ecsLocalSimulation) Build(ctx context.Context, project *types.Project) error {
-	return errdefs.ErrNotImplemented
+	return e.compose.Build(ctx, project)
 }
 
 func (e ecsLocalSimulation) Push(ctx context.Context, project *types.Project) error {
-	return errdefs.ErrNotImplemented
+	return e.compose.Push(ctx, project)
 }
 
 func (e ecsLocalSimulation) Pull(ctx context.Context, project *types.Project) error {
-	return errdefs.ErrNotImplemented
+	return e.compose.Pull(ctx, project)
 }
 
 func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project) error {
-	return errdefs.ErrNotImplemented
+	enhanced, err := e.enhanceForLocalSimulation(project)
+	if err != nil {
+		return err
+	}
+
+	return e.compose.Create(ctx, enhanced)
 }
 
 func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
-	return errdefs.ErrNotImplemented
+	return e.compose.Start(ctx, project, consumer)
 }
 
-func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, detach bool) error {
+func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
+	return errdefs.ErrNotImplemented
+}
 
-	cmd := exec.Command("docker-compose", "version", "--short")
-	b := bytes.Buffer{}
-	b.WriteString("v")
-	cmd.Stdout = bufio.NewWriter(&b)
-	err := cmd.Run()
+func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
+	enhanced, err := e.enhanceForLocalSimulation(project)
 	if err != nil {
-		return errors.Wrap(err, "ECS simulation mode require Docker-compose 1.27")
-	}
-	version := semver.MajorMinor(strings.TrimSpace(b.String()))
-	if version == "" {
-		return fmt.Errorf("can't parse docker-compose version: %s", b.String())
-	}
-	if semver.Compare(version, "v1.27") < 0 {
-		return fmt.Errorf("ECS simulation mode require Docker-compose 1.27, found %s", version)
+		return nil, err
 	}
 
-	converted, err := e.Convert(ctx, project, "json")
-	if err != nil {
-		return err
+	delete(enhanced.Networks, "default")
+	config := map[string]interface{}{
+		"services": enhanced.Services,
+		"networks": enhanced.Networks,
+		"volumes":  enhanced.Volumes,
+		"secrets":  enhanced.Secrets,
+		"configs":  enhanced.Configs,
+	}
+	switch options.Format {
+	case "json":
+		return json.MarshalIndent(config, "", "  ")
+	case "yaml":
+		return yaml.Marshal(config)
+	default:
+		return nil, fmt.Errorf("unsupported format %q", options)
 	}
 
-	cmd = exec.Command("docker-compose", "--context", "default", "--project-directory", project.WorkingDir, "--project-name", project.Name, "-f", "-", "up")
-	cmd.Stdin = strings.NewReader(string(converted))
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	return cmd.Run()
 }
 
-func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
+func (e ecsLocalSimulation) enhanceForLocalSimulation(project *types.Project) (*types.Project, error) {
 	project.Networks["credentials_network"] = types.NetworkConfig{
+		Name:   "credentials_network",
 		Driver: "bridge",
 		Ipam: types.IPAMConfig{
 			Config: []*types.IPAMPool{
@@ -148,68 +144,21 @@ func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project,
 			},
 		},
 	})
-
-	delete(project.Networks, "default")
-	config := map[string]interface{}{
-		"services": project.Services,
-		"networks": project.Networks,
-		"volumes":  project.Volumes,
-		"secrets":  project.Secrets,
-		"configs":  project.Configs,
-	}
-	switch format {
-	case "json":
-		return json.MarshalIndent(config, "", "  ")
-	case "yaml":
-		return yaml.Marshal(config)
-	default:
-		return nil, fmt.Errorf("unsupported format %q", format)
-	}
-
+	return project, nil
 }
 
-func (e ecsLocalSimulation) Down(ctx context.Context, projectName string) error {
-	cmd := exec.Command("docker-compose", "--context", "default", "--project-name", projectName, "-f", "-", "down", "--remove-orphans")
-	cmd.Stdin = strings.NewReader(string(`
-services:
-   ecs-local-endpoints:
-      image: "amazon/amazon-ecs-local-container-endpoints"
-`))
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	return cmd.Run()
+func (e ecsLocalSimulation) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
+	options.RemoveOrphans = true
+	return e.compose.Down(ctx, projectName, options)
 }
 
 func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer, options compose.LogOptions) error {
-	list, err := e.moby.ContainerList(ctx, types2.ContainerListOptions{
-		Filters: filters.NewArgs(filters.Arg("label", "com.docker.compose.project="+projectName)),
-	})
-	if err != nil {
-		return err
-	}
-	services := map[string]types.ServiceConfig{}
-	for _, c := range list {
-		services[c.Labels["com.docker.compose.service"]] = types.ServiceConfig{
-			Image: "unused",
-		}
-	}
-
-	marshal, err := yaml.Marshal(map[string]interface{}{
-		"services": services,
-	})
-	if err != nil {
-		return err
-	}
-	cmd := exec.Command("docker-compose", "--context", "default", "--project-name", projectName, "-f", "-", "logs", "-f")
-	cmd.Stdin = strings.NewReader(string(marshal))
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	return cmd.Run()
+	return e.compose.Logs(ctx, projectName, consumer, options)
 }
 
 func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
-	return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps")
+	return e.compose.Ps(ctx, projectName)
 }
 func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
-	return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ls")
+	return e.compose.List(ctx, projectName)
 }

+ 4 - 4
ecs/up.go

@@ -49,14 +49,14 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, consu
 	return errdefs.ErrNotImplemented
 }
 
-func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach bool) error {
+func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 
 	err := b.aws.CheckRequirements(ctx, b.Region)
 	if err != nil {
 		return err
 	}
 
-	template, err := b.Convert(ctx, project, "yaml")
+	template, err := b.Convert(ctx, project, compose.ConvertOptions{Format: "yaml"})
 	if err != nil {
 		return err
 	}
@@ -82,7 +82,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
 			return err
 		}
 	}
-	if detach {
+	if options.Detach {
 		return nil
 	}
 	signalChan := make(chan os.Signal, 1)
@@ -90,7 +90,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b
 	go func() {
 		<-signalChan
 		fmt.Println("user interrupted deployment. Deleting stack...")
-		b.Down(ctx, project.Name) // nolint:errcheck
+		b.Down(ctx, project.Name, compose.DownOptions{}) // nolint:errcheck
 	}()
 
 	err = b.WaitStackCompletion(ctx, project.Name, operation)

+ 4 - 4
example/backend.go

@@ -158,14 +158,14 @@ func (cs *composeService) Start(ctx context.Context, project *types.Project, con
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
+func (cs *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 
 	fmt.Printf("Up command on project %q", project.Name)
 	return nil
 }
 
-func (cs *composeService) Down(ctx context.Context, project string) error {
-	fmt.Printf("Down command on project %q", project)
+func (cs *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
+	fmt.Printf("Down command on project %q", projectName)
 	return nil
 }
 
@@ -179,6 +179,6 @@ func (cs *composeService) Logs(ctx context.Context, projectName string, consumer
 	return errdefs.ErrNotImplemented
 }
 
-func (cs *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
+func (cs *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
 	return nil, errdefs.ErrNotImplemented
 }

+ 0 - 1
go.mod

@@ -51,7 +51,6 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.6.1
 	github.com/valyala/fasttemplate v1.2.1 // indirect
-	golang.org/x/mod v0.3.0
 	golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
 	golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58
 	golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9

+ 4 - 4
local/compose/compose.go

@@ -40,7 +40,7 @@ type composeService struct {
 	apiClient *client.Client
 }
 
-func (s *composeService) Up(ctx context.Context, project *types.Project, detach bool) error {
+func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	return errdefs2.ErrNotImplemented
 }
 
@@ -54,13 +54,13 @@ func getContainerName(c moby.Container) string {
 	return c.Names[0][1:]
 }
 
-func (s *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
-	switch format {
+func (s *composeService) Convert(ctx context.Context, project *types.Project, options compose.ConvertOptions) ([]byte, error) {
+	switch options.Format {
 	case "json":
 		return json.MarshalIndent(project, "", "  ")
 	case "yaml":
 		return yaml.Marshal(project)
 	default:
-		return nil, fmt.Errorf("unsupported format %q", format)
+		return nil, fmt.Errorf("unsupported format %q", options)
 	}
 }

+ 45 - 11
local/compose/down.go

@@ -21,6 +21,8 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	"github.com/docker/compose-cli/progress"
 
 	"github.com/compose-spec/compose-go/cli"
@@ -30,7 +32,7 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-func (s *composeService) Down(ctx context.Context, projectName string) error {
+func (s *composeService) Down(ctx context.Context, projectName string, options compose.DownOptions) error {
 	eg, _ := errgroup.WithContext(ctx)
 	w := progress.ContextWriter(ctx)
 
@@ -39,11 +41,28 @@ func (s *composeService) Down(ctx context.Context, projectName string) error {
 		return err
 	}
 
+	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+		Filters: filters.NewArgs(projectFilter(project.Name)),
+		All:     true,
+	})
+	if err != nil {
+		return err
+	}
+
 	err = InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		filter := filters.NewArgs(projectFilter(project.Name), serviceFilter(service.Name))
-		return s.removeContainers(ctx, w, eg, filter)
+		serviceContainers, others := split(containers, isService(service.Name))
+		err := s.removeContainers(ctx, w, eg, serviceContainers)
+		containers = others
+		return err
 	})
 
+	if options.RemoveOrphans {
+		err := s.removeContainers(ctx, w, eg, containers)
+		if err != nil {
+			return err
+		}
+	}
+
 	if err != nil {
 		return err
 	}
@@ -70,14 +89,7 @@ func (s *composeService) Down(ctx context.Context, projectName string) error {
 	return eg.Wait()
 }
 
-func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, filter filters.Args) error {
-	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
-		Filters: filter,
-		All:     true,
-	})
-	if err != nil {
-		return err
-	}
+func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
 	for _, container := range containers {
 		eg.Go(func() error {
 			eventName := "Container " + getContainerName(container)
@@ -147,3 +159,25 @@ func loadProjectOptionsFromLabels(c moby.Container) (*cli.ProjectOptions, error)
 		cli.WithWorkingDirectory(c.Labels[workingDirLabel]),
 		cli.WithName(c.Labels[projectLabel]))
 }
+
+type containerPredicate func(c moby.Container) bool
+
+func isService(service string) containerPredicate {
+	return func(c moby.Container) bool {
+		return c.Labels[serviceLabel] == service
+	}
+}
+
+// split return a container slice with elements to match predicate
+func split(containers []moby.Container, predicate containerPredicate) ([]moby.Container, []moby.Container) {
+	var right []moby.Container
+	var left []moby.Container
+	for _, c := range containers {
+		if predicate(c) {
+			right = append(right, c)
+		} else {
+			left = append(left, c)
+		}
+	}
+	return right, left
+}

+ 1 - 0
local/compose/logs.go

@@ -34,6 +34,7 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer
 		Filters: filters.NewArgs(
 			projectFilter(projectName),
 		),
+		All: true,
 	})
 
 	ignore := func(string) bool {

+ 6 - 2
server/proxy/compose.go

@@ -19,6 +19,8 @@ package proxy
 import (
 	"context"
 
+	"github.com/docker/compose-cli/api/compose"
+
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/compose-spec/compose-go/types"
 
@@ -30,7 +32,8 @@ func (p *proxy) Up(ctx context.Context, request *composev1.ComposeUpRequest) (*c
 	if err != nil {
 		return nil, err
 	}
-	return &composev1.ComposeUpResponse{ProjectName: project.Name}, Client(ctx).ComposeService().Up(ctx, project, true)
+	err = Client(ctx).ComposeService().Up(ctx, project, compose.UpOptions{Detach: true})
+	return &composev1.ComposeUpResponse{ProjectName: project.Name}, err
 }
 
 func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest) (*composev1.ComposeDownResponse, error) {
@@ -42,7 +45,8 @@ func (p *proxy) Down(ctx context.Context, request *composev1.ComposeDownRequest)
 		}
 		projectName = project.Name
 	}
-	return &composev1.ComposeDownResponse{ProjectName: projectName}, Client(ctx).ComposeService().Down(ctx, projectName)
+	err := Client(ctx).ComposeService().Down(ctx, projectName, compose.DownOptions{})
+	return &composev1.ComposeDownResponse{ProjectName: projectName}, err
 }
 
 func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServicesRequest) (*composev1.ComposeServicesResponse, error) {

+ 7 - 0
tests/compose-e2e/fixtures/logs-test/compose.yaml

@@ -0,0 +1,7 @@
+services:
+  ping:
+    image: busybox:1.27.2
+    command: ping localhost -c 1
+  hello:
+    image: busybox:1.31.0-uclibc
+    command: echo hello

+ 60 - 0
tests/compose-e2e/logs_test.go

@@ -0,0 +1,60 @@
+/*
+   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 e2e
+
+import (
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+
+	"gotest.tools/v3/icmd"
+
+	. "github.com/docker/compose-cli/tests/framework"
+)
+
+func TestLocalComposeLogs(t *testing.T) {
+	c := NewParallelE2eCLI(t, binDir)
+
+	const projectName = "compose-e2e-logs"
+
+	t.Run("up", func(t *testing.T) {
+		c.RunDockerCmd("compose", "up", "-d", "-f", "./fixtures/logs-test/compose.yaml", "--project-name", projectName, "-d")
+	})
+
+	t.Run("logs", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "logs", "--project-name", projectName)
+		res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
+		res.Assert(t, icmd.Expected{Out: `hello`})
+	})
+
+	t.Run("logs ping", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "logs", "--project-name", projectName, "ping")
+		res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
+		assert.Assert(t, !strings.Contains(res.Stdout(), "hello"))
+	})
+
+	t.Run("logs hello", func(t *testing.T) {
+		res := c.RunDockerCmd("compose", "logs", "--project-name", projectName, "hello", "ping")
+		res.Assert(t, icmd.Expected{Out: `PING localhost (127.0.0.1)`})
+		res.Assert(t, icmd.Expected{Out: `hello`})
+	})
+
+	t.Run("down", func(t *testing.T) {
+		_ = c.RunDockerCmd("compose", "down", "--project-name", projectName)
+	})
+}