From 0b517741a063a548af42daecd3dd8482bc989114 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 15 Feb 2021 13:54:55 +0100 Subject: [PATCH] introduce --timeout on compose stop|down Signed-off-by: Nicolas De Loof --- aci/compose.go | 2 +- api/client/compose.go | 2 +- api/compose/api.go | 11 ++++++- cli/cmd/compose/down.go | 11 +++++++ cli/cmd/compose/remove.go | 2 +- cli/cmd/compose/stop.go | 21 +++++++++++-- cli/cmd/compose/up.go | 2 +- ecs/local/compose.go | 4 +-- ecs/up.go | 2 +- kube/compose.go | 2 +- local/compose/create.go | 2 +- local/compose/down.go | 13 ++++---- local/compose/down_test.go | 9 +++--- local/compose/stop.go | 10 +++--- local/compose/stop_test.go | 62 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 local/compose/stop_test.go diff --git a/aci/compose.go b/aci/compose.go index c7f82aba8..7d1780eb9 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -64,7 +64,7 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, return errdefs.ErrNotImplemented } -func (cs *aciComposeService) Stop(ctx context.Context, project *types.Project) error { +func (cs *aciComposeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/client/compose.go b/api/client/compose.go index 063da2081..912fb08b3 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -48,7 +48,7 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, opti return errdefs.ErrNotImplemented } -func (c *composeService) Stop(ctx context.Context, project *types.Project) error { +func (c *composeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/api/compose/api.go b/api/compose/api.go index 65cbbbe7f..3d2f39ee9 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -19,6 +19,7 @@ package compose import ( "context" "io" + "time" "github.com/compose-spec/compose-go/types" ) @@ -36,7 +37,7 @@ type Service interface { // Start executes the equivalent to a `compose start` Start(ctx context.Context, project *types.Project, options StartOptions) error // Stop executes the equivalent to a `compose stop` - Stop(ctx context.Context, project *types.Project) error + Stop(ctx context.Context, project *types.Project, options StopOptions) 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` @@ -71,6 +72,12 @@ type StartOptions struct { Attach ContainerEventListener } +// StopOptions group options of the Stop API +type StopOptions struct { + // Timeout override container stop timeout + Timeout *time.Duration +} + // UpOptions group options of the Up API type UpOptions struct { // Detach will create services and return immediately @@ -83,6 +90,8 @@ type DownOptions struct { RemoveOrphans bool // Project is the compose project used to define this app. Might be nil if user ran `down` just with project name Project *types.Project + // Timeout override container stop timeout + Timeout *time.Duration } // ConvertOptions group options of the Convert API diff --git a/cli/cmd/compose/down.go b/cli/cmd/compose/down.go index ca92e96f5..44de0c880 100644 --- a/cli/cmd/compose/down.go +++ b/cli/cmd/compose/down.go @@ -18,6 +18,7 @@ package compose import ( "context" + "time" "github.com/compose-spec/compose-go/types" @@ -32,6 +33,8 @@ import ( type downOptions struct { *projectOptions removeOrphans bool + timeChanged bool + timeout int } func downCommand(p *projectOptions) *cobra.Command { @@ -42,11 +45,13 @@ func downCommand(p *projectOptions) *cobra.Command { Use: "down", Short: "Stop and remove containers, networks", RunE: func(cmd *cobra.Command, args []string) error { + opts.timeChanged = cmd.Flags().Changed("timeout") return runDown(cmd.Context(), opts) }, } flags := downCmd.Flags() flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") + flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") return downCmd } @@ -69,9 +74,15 @@ func runDown(ctx context.Context, opts downOptions) error { name = p.Name } + var timeout *time.Duration + if opts.timeChanged { + timeoutValue := time.Duration(opts.timeout) * time.Second + timeout = &timeoutValue + } return name, c.ComposeService().Down(ctx, name, compose.DownOptions{ RemoveOrphans: opts.removeOrphans, Project: project, + Timeout: timeout, }) }) return err diff --git a/cli/cmd/compose/remove.go b/cli/cmd/compose/remove.go index af5040136..02a0e0b8c 100644 --- a/cli/cmd/compose/remove.go +++ b/cli/cmd/compose/remove.go @@ -73,7 +73,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error if opts.stop { _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - err := c.ComposeService().Stop(ctx, project) + err := c.ComposeService().Stop(ctx, project, compose.StopOptions{}) return "", err }) if err != nil { diff --git a/cli/cmd/compose/stop.go b/cli/cmd/compose/stop.go index f3bb29d36..a0c87007e 100644 --- a/cli/cmd/compose/stop.go +++ b/cli/cmd/compose/stop.go @@ -18,29 +18,37 @@ package compose import ( "context" + "time" "github.com/spf13/cobra" "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/progress" ) type stopOptions struct { *projectOptions + timeChanged bool + timeout int } func stopCommand(p *projectOptions) *cobra.Command { opts := stopOptions{ projectOptions: p, } - stopCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "stop [SERVICE...]", Short: "Stop services", RunE: func(cmd *cobra.Command, args []string) error { + opts.timeChanged = cmd.Flags().Changed("timeout") return runStop(cmd.Context(), opts, args) }, } - return stopCmd + flags := cmd.Flags() + flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds") + + return cmd } func runStop(ctx context.Context, opts stopOptions, services []string) error { @@ -54,8 +62,15 @@ func runStop(ctx context.Context, opts stopOptions, services []string) error { return err } + var timeout *time.Duration + if opts.timeChanged { + timeoutValue := time.Duration(opts.timeout) * time.Second + timeout = &timeoutValue + } _, err = progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Stop(ctx, project) + return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{ + Timeout: timeout, + }) }) return err } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 4da5e1bc1..97358be90 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -192,7 +192,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro stopFunc := func() error { ctx := context.Background() _, err := progress.Run(ctx, func(ctx context.Context) (string, error) { - return "", c.ComposeService().Stop(ctx, project) + return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{}) }) return err } diff --git a/ecs/local/compose.go b/ecs/local/compose.go index 1cd0c9b60..15212e9eb 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -57,8 +57,8 @@ func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, o return e.compose.Start(ctx, project, options) } -func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project) error { - return e.compose.Stop(ctx, project) +func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { + return e.compose.Stop(ctx, project, options) } func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { diff --git a/ecs/up.go b/ecs/up.go index f66bca71a..343317e53 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -51,7 +51,7 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, optio return errdefs.ErrNotImplemented } -func (b *ecsAPIService) Stop(ctx context.Context, project *types.Project) error { +func (b *ecsAPIService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/kube/compose.go b/kube/compose.go index 77f1eceea..62b8615aa 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -149,7 +149,7 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, opti } // Stop executes the equivalent to a `compose stop` -func (s *composeService) Stop(ctx context.Context, project *types.Project) error { +func (s *composeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { return errdefs.ErrNotImplemented } diff --git a/local/compose/create.go b/local/compose/create.go index 01bec5ed7..5b0e121b5 100644 --- a/local/compose/create.go +++ b/local/compose/create.go @@ -80,7 +80,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt if len(orphans) > 0 { if opts.RemoveOrphans { w := progress.ContextWriter(ctx) - err := s.removeContainers(ctx, w, orphans) + err := s.removeContainers(ctx, w, orphans, nil) if err != nil { return err } diff --git a/local/compose/down.go b/local/compose/down.go index 6e7cc1e92..d3b1048c6 100644 --- a/local/compose/down.go +++ b/local/compose/down.go @@ -20,6 +20,7 @@ import ( "context" "path/filepath" "strings" + "time" "github.com/docker/compose-cli/api/compose" @@ -58,7 +59,7 @@ 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 := containers.filter(isService(service.Name)) - err := s.removeContainers(ctx, w, serviceContainers) + err := s.removeContainers(ctx, w, serviceContainers, options.Timeout) return err }) if err != nil { @@ -67,7 +68,7 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c orphans := containers.filter(isNotService(options.Project.ServiceNames()...)) if options.RemoveOrphans && len(orphans) > 0 { - err := s.removeContainers(ctx, w, orphans) + err := s.removeContainers(ctx, w, orphans, options.Timeout) if err != nil { return err } @@ -93,12 +94,12 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c return eg.Wait() } -func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container) error { +func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error { for _, container := range containers { toStop := container eventName := getContainerProgressName(toStop) w.Event(progress.StoppingEvent(eventName)) - err := s.apiClient.ContainerStop(ctx, toStop.ID, nil) + err := s.apiClient.ContainerStop(ctx, toStop.ID, timeout) if err != nil { w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) return err @@ -108,14 +109,14 @@ func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, return nil } -func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container) error { +func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error { eg, _ := errgroup.WithContext(ctx) for _, container := range containers { toDelete := container eg.Go(func() error { eventName := getContainerProgressName(toDelete) w.Event(progress.StoppingEvent(eventName)) - err := s.stopContainers(ctx, w, []moby.Container{toDelete}) + err := s.stopContainers(ctx, w, []moby.Container{toDelete}, timeout) if err != nil { w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) return err diff --git a/local/compose/down_test.go b/local/compose/down_test.go index 7ebf0af9e..64ed23aa3 100644 --- a/local/compose/down_test.go +++ b/local/compose/down_test.go @@ -20,14 +20,13 @@ import ( "context" "testing" - "github.com/golang/mock/gomock" - "gotest.tools/v3/assert" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/local/mocks" apitypes "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/local/mocks" + "github.com/golang/mock/gomock" + "gotest.tools/v3/assert" ) func TestDown(t *testing.T) { diff --git a/local/compose/stop.go b/local/compose/stop.go index b1436f33e..5ecc50160 100644 --- a/local/compose/stop.go +++ b/local/compose/stop.go @@ -19,16 +19,16 @@ package compose import ( "context" + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/progress" + "github.com/compose-spec/compose-go/types" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - - "github.com/docker/compose-cli/api/progress" ) -func (s *composeService) Stop(ctx context.Context, project *types.Project) error { +func (s *composeService) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error { w := progress.ContextWriter(ctx) - var containers Containers containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ Filters: filters.NewArgs(projectFilter(project.Name)), @@ -41,6 +41,6 @@ func (s *composeService) Stop(ctx context.Context, project *types.Project) error containers = containers.filter(isService(project.ServiceNames()...)) return InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { - return s.stopContainers(ctx, w, containers.filter(isService(service.Name))) + return s.stopContainers(ctx, w, containers.filter(isService(service.Name)), options.Timeout) }) } diff --git a/local/compose/stop_test.go b/local/compose/stop_test.go new file mode 100644 index 000000000..1afce2d43 --- /dev/null +++ b/local/compose/stop_test.go @@ -0,0 +1,62 @@ +/* + 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" + "testing" + "time" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/local/mocks" + moby "github.com/docker/docker/api/types" + + "github.com/compose-spec/compose-go/types" + "github.com/golang/mock/gomock" + "gotest.tools/v3/assert" +) + +func TestStopTimeout(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + api := mocks.NewMockAPIClient(mockCtrl) + tested.apiClient = api + + ctx := context.Background() + api.EXPECT().ContainerList(ctx, projectFilterListOpt(testProject)).Return( + []moby.Container{ + testContainer("service1", "123"), + testContainer("service1", "456"), + testContainer("service2", "789"), + }, nil) + + timeout := time.Duration(2) * time.Second + api.EXPECT().ContainerStop(ctx, "123", &timeout).Return(nil) + api.EXPECT().ContainerStop(ctx, "456", &timeout).Return(nil) + api.EXPECT().ContainerStop(ctx, "789", &timeout).Return(nil) + + err := tested.Stop(ctx, &types.Project{ + Name: testProject, + Services: []types.ServiceConfig{ + {Name: "service1"}, + {Name: "service2"}, + }, + }, compose.StopOptions{ + Timeout: &timeout, + }) + assert.NilError(t, err) +}