From 0529415fa7148284f9b1bfb4245e3992f711b0a6 Mon Sep 17 00:00:00 2001
From: Ulysses Souza <ulyssessouza@gmail.com>
Date: Wed, 20 Jan 2021 09:35:06 -0300
Subject: [PATCH] Stop project on Ctrl+C

Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
---
 aci/compose.go          |  4 ++++
 api/client/compose.go   |  4 ++++
 api/compose/api.go      |  2 ++
 api/progress/event.go   |  7 +++++-
 cli/cmd/compose/up.go   |  2 +-
 ecs/local/compose.go    |  4 ++++
 ecs/up.go               |  4 ++++
 local/compose/create.go |  5 +++-
 local/compose/down.go   | 29 +++++++++++++++-------
 local/compose/stop.go   | 53 +++++++++++++++++++++++++++++++++++++++++
 10 files changed, 102 insertions(+), 12 deletions(-)
 create mode 100644 local/compose/stop.go

diff --git a/aci/compose.go b/aci/compose.go
index 60a42b170..18a538ba9 100644
--- a/aci/compose.go
+++ b/aci/compose.go
@@ -64,6 +64,10 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project,
 	return errdefs.ErrNotImplemented
 }
 
+func (cs *aciComposeService) Stop(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	logrus.Debugf("Up on project with name %q", project.Name)
 
diff --git a/api/client/compose.go b/api/client/compose.go
index 072b06b99..4d33b937f 100644
--- a/api/client/compose.go
+++ b/api/client/compose.go
@@ -48,6 +48,10 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, cons
 	return errdefs.ErrNotImplemented
 }
 
+func (c *composeService) Stop(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (c *composeService) Up(context.Context, *types.Project, compose.UpOptions) error {
 	return errdefs.ErrNotImplemented
 }
diff --git a/api/compose/api.go b/api/compose/api.go
index 19a535b22..939aab132 100644
--- a/api/compose/api.go
+++ b/api/compose/api.go
@@ -35,6 +35,8 @@ type Service interface {
 	Create(ctx context.Context, project *types.Project, opts CreateOptions) error
 	// Start executes the equivalent to a `compose start`
 	Start(ctx context.Context, project *types.Project, consumer LogConsumer) error
+	// Stop executes the equivalent to a `compose stop`
+	Stop(ctx context.Context, project *types.Project, consumer LogConsumer) error
 	// Up executes the equivalent to a `compose up`
 	Up(ctx context.Context, project *types.Project, options UpOptions) error
 	// Down executes the equivalent to a `compose down`
diff --git a/api/progress/event.go b/api/progress/event.go
index f78508152..c8473decc 100644
--- a/api/progress/event.go
+++ b/api/progress/event.go
@@ -78,11 +78,16 @@ func CreatedEvent(ID string) Event {
 	return NewEvent(ID, Done, "Created")
 }
 
-// StoppingEvent stops a new Removing in progress Event
+// StoppingEvent stops a new Stopping in progress Event
 func StoppingEvent(ID string) Event {
 	return NewEvent(ID, Working, "Stopping")
 }
 
+// StoppedEvent stops a new Stopping in progress Event
+func StoppedEvent(ID string) Event {
+	return NewEvent(ID, Done, "Stopped")
+}
+
 // RemovingEvent creates a new Removing in progress Event
 func RemovingEvent(ID string) Event {
 	return NewEvent(ID, Working, "Removing")
diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go
index 07656fedc..ed606a1cd 100644
--- a/cli/cmd/compose/up.go
+++ b/cli/cmd/compose/up.go
@@ -106,7 +106,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 		fmt.Println("Gracefully stopping...")
 		ctx = context.Background()
 		_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-			return "", c.ComposeService().Down(ctx, project.Name, compose.DownOptions{})
+			return "", c.ComposeService().Stop(ctx, project, consumer)
 		})
 	}
 	return err
diff --git a/ecs/local/compose.go b/ecs/local/compose.go
index b20741afe..8df453859 100644
--- a/ecs/local/compose.go
+++ b/ecs/local/compose.go
@@ -57,6 +57,10 @@ func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, c
 	return e.compose.Start(ctx, project, consumer)
 }
 
+func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
+	return e.compose.Stop(ctx, project, consumer)
+}
+
 func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	return errdefs.ErrNotImplemented
 }
diff --git a/ecs/up.go b/ecs/up.go
index 6a71b1bfa..5b6325a43 100644
--- a/ecs/up.go
+++ b/ecs/up.go
@@ -51,6 +51,10 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, consu
 	return errdefs.ErrNotImplemented
 }
 
+func (b *ecsAPIService) Stop(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	logrus.Debugf("deploying on AWS with region=%q", b.Region)
 	err := b.aws.CheckRequirements(ctx, b.Region)
diff --git a/local/compose/create.go b/local/compose/create.go
index 643718df6..35cf3632a 100644
--- a/local/compose/create.go
+++ b/local/compose/create.go
@@ -79,7 +79,10 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
 		if opts.RemoveOrphans {
 			eg, _ := errgroup.WithContext(ctx)
 			w := progress.ContextWriter(ctx)
-			s.removeContainers(ctx, w, eg, orphans)
+			err := s.removeContainers(ctx, w, eg, orphans)
+			if err != nil {
+				return err
+			}
 			if eg.Wait() != nil {
 				return err
 			}
diff --git a/local/compose/down.go b/local/compose/down.go
index 4bfcbd323..14d3bc834 100644
--- a/local/compose/down.go
+++ b/local/compose/down.go
@@ -55,13 +55,13 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
 
 	err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service types.ServiceConfig) error {
 		serviceContainers, others := containers.split(isService(service.Name))
-		s.removeContainers(ctx, w, eg, serviceContainers)
+		err := s.removeContainers(ctx, w, eg, serviceContainers)
 		containers = others
 		return err
 	})
 
 	if options.RemoveOrphans {
-		s.removeContainers(ctx, w, eg, containers)
+		err := s.removeContainers(ctx, w, eg, containers)
 		if err != nil {
 			return err
 		}
@@ -93,17 +93,27 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
 	return eg.Wait()
 }
 
-func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) {
+func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container) error {
+	for _, container := range containers {
+		toStop := container
+		eventName := "Container " + getCanonicalContainerName(toStop)
+		w.Event(progress.StoppingEvent(eventName))
+		err := s.apiClient.ContainerStop(ctx, toStop.ID, nil)
+		if err != nil {
+			w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
+			return err
+		}
+		w.Event(progress.StoppedEvent(eventName))
+	}
+	return nil
+}
+
+func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, eg *errgroup.Group, containers []moby.Container) error {
 	for _, container := range containers {
 		toDelete := container
 		eg.Go(func() error {
 			eventName := "Container " + getCanonicalContainerName(toDelete)
-			w.Event(progress.StoppingEvent(eventName))
-			err := s.apiClient.ContainerStop(ctx, toDelete.ID, nil)
-			if err != nil {
-				w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
-				return err
-			}
+			err := s.stopContainers(ctx, w, []moby.Container{container})
 			w.Event(progress.RemovingEvent(eventName))
 			err = s.apiClient.ContainerRemove(ctx, toDelete.ID, moby.ContainerRemoveOptions{Force: true})
 			if err != nil {
@@ -114,6 +124,7 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
 			return nil
 		})
 	}
+	return eg.Wait()
 }
 
 func (s *composeService) projectFromContainerLabels(ctx context.Context, projectName string) (*types.Project, error) {
diff --git a/local/compose/stop.go b/local/compose/stop.go
new file mode 100644
index 000000000..cb616d8ab
--- /dev/null
+++ b/local/compose/stop.go
@@ -0,0 +1,53 @@
+/*
+   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"
+
+	moby "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/progress"
+
+	"github.com/compose-spec/compose-go/types"
+	"golang.org/x/sync/errgroup"
+)
+
+func (s *composeService) Stop(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
+	eg, _ := errgroup.WithContext(ctx)
+	w := progress.ContextWriter(ctx)
+
+	var containers Containers
+	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 {
+		serviceContainers, others := containers.split(isService(service.Name))
+		err := s.stopContainers(ctx, w, serviceContainers)
+		containers = others
+		return err
+	})
+
+	return eg.Wait()
+}