introduce --timeout on compose stop|down

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2021-02-15 13:54:55 +01:00
parent de586871de
commit 0b517741a0
15 changed files with 126 additions and 29 deletions

View File

@ -64,7 +64,7 @@ func (cs *aciComposeService) Start(ctx context.Context, project *types.Project,
return errdefs.ErrNotImplemented 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 return errdefs.ErrNotImplemented
} }

View File

@ -48,7 +48,7 @@ func (c *composeService) Start(ctx context.Context, project *types.Project, opti
return errdefs.ErrNotImplemented 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 return errdefs.ErrNotImplemented
} }

View File

@ -19,6 +19,7 @@ package compose
import ( import (
"context" "context"
"io" "io"
"time"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
) )
@ -36,7 +37,7 @@ type Service interface {
// Start executes the equivalent to a `compose start` // Start executes the equivalent to a `compose start`
Start(ctx context.Context, project *types.Project, options StartOptions) error Start(ctx context.Context, project *types.Project, options StartOptions) error
// Stop executes the equivalent to a `compose stop` // 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 executes the equivalent to a `compose up`
Up(ctx context.Context, project *types.Project, options UpOptions) error Up(ctx context.Context, project *types.Project, options UpOptions) error
// Down executes the equivalent to a `compose down` // Down executes the equivalent to a `compose down`
@ -71,6 +72,12 @@ type StartOptions struct {
Attach ContainerEventListener 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 // UpOptions group options of the Up API
type UpOptions struct { type UpOptions struct {
// Detach will create services and return immediately // Detach will create services and return immediately
@ -83,6 +90,8 @@ type DownOptions struct {
RemoveOrphans bool RemoveOrphans bool
// Project is the compose project used to define this app. Might be nil if user ran `down` just with project name // Project is the compose project used to define this app. Might be nil if user ran `down` just with project name
Project *types.Project Project *types.Project
// Timeout override container stop timeout
Timeout *time.Duration
} }
// ConvertOptions group options of the Convert API // ConvertOptions group options of the Convert API

View File

@ -18,6 +18,7 @@ package compose
import ( import (
"context" "context"
"time"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
@ -32,6 +33,8 @@ import (
type downOptions struct { type downOptions struct {
*projectOptions *projectOptions
removeOrphans bool removeOrphans bool
timeChanged bool
timeout int
} }
func downCommand(p *projectOptions) *cobra.Command { func downCommand(p *projectOptions) *cobra.Command {
@ -42,11 +45,13 @@ func downCommand(p *projectOptions) *cobra.Command {
Use: "down", Use: "down",
Short: "Stop and remove containers, networks", Short: "Stop and remove containers, networks",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
return runDown(cmd.Context(), opts) return runDown(cmd.Context(), opts)
}, },
} }
flags := downCmd.Flags() flags := downCmd.Flags()
flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.") 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 return downCmd
} }
@ -69,9 +74,15 @@ func runDown(ctx context.Context, opts downOptions) error {
name = p.Name 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{ return name, c.ComposeService().Down(ctx, name, compose.DownOptions{
RemoveOrphans: opts.removeOrphans, RemoveOrphans: opts.removeOrphans,
Project: project, Project: project,
Timeout: timeout,
}) })
}) })
return err return err

View File

@ -73,7 +73,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error
if opts.stop { if opts.stop {
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) { _, 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 return "", err
}) })
if err != nil { if err != nil {

View File

@ -18,29 +18,37 @@ package compose
import ( import (
"context" "context"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/client"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/progress" "github.com/docker/compose-cli/api/progress"
) )
type stopOptions struct { type stopOptions struct {
*projectOptions *projectOptions
timeChanged bool
timeout int
} }
func stopCommand(p *projectOptions) *cobra.Command { func stopCommand(p *projectOptions) *cobra.Command {
opts := stopOptions{ opts := stopOptions{
projectOptions: p, projectOptions: p,
} }
stopCmd := &cobra.Command{ cmd := &cobra.Command{
Use: "stop [SERVICE...]", Use: "stop [SERVICE...]",
Short: "Stop services", Short: "Stop services",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.timeChanged = cmd.Flags().Changed("timeout")
return runStop(cmd.Context(), opts, args) 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 { 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 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) { _, 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 return err
} }

View File

@ -192,7 +192,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
stopFunc := func() error { stopFunc := func() error {
ctx := context.Background() ctx := context.Background()
_, err := progress.Run(ctx, func(ctx context.Context) (string, error) { _, 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 return err
} }

View File

@ -57,8 +57,8 @@ func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, o
return e.compose.Start(ctx, project, options) return e.compose.Start(ctx, project, options)
} }
func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project) error { func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project, options compose.StopOptions) error {
return e.compose.Stop(ctx, project) return e.compose.Stop(ctx, project, options)
} }
func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {

View File

@ -51,7 +51,7 @@ func (b *ecsAPIService) Start(ctx context.Context, project *types.Project, optio
return errdefs.ErrNotImplemented 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 return errdefs.ErrNotImplemented
} }

View File

@ -149,7 +149,7 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, opti
} }
// Stop executes the equivalent to a `compose stop` // 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 return errdefs.ErrNotImplemented
} }

View File

@ -80,7 +80,7 @@ func (s *composeService) Create(ctx context.Context, project *types.Project, opt
if len(orphans) > 0 { if len(orphans) > 0 {
if opts.RemoveOrphans { if opts.RemoveOrphans {
w := progress.ContextWriter(ctx) w := progress.ContextWriter(ctx)
err := s.removeContainers(ctx, w, orphans) err := s.removeContainers(ctx, w, orphans, nil)
if err != nil { if err != nil {
return err return err
} }

View File

@ -20,6 +20,7 @@ import (
"context" "context"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/docker/compose-cli/api/compose" "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 { err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service types.ServiceConfig) error {
serviceContainers := containers.filter(isService(service.Name)) serviceContainers := containers.filter(isService(service.Name))
err := s.removeContainers(ctx, w, serviceContainers) err := s.removeContainers(ctx, w, serviceContainers, options.Timeout)
return err return err
}) })
if err != nil { 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()...)) orphans := containers.filter(isNotService(options.Project.ServiceNames()...))
if options.RemoveOrphans && len(orphans) > 0 { if options.RemoveOrphans && len(orphans) > 0 {
err := s.removeContainers(ctx, w, orphans) err := s.removeContainers(ctx, w, orphans, options.Timeout)
if err != nil { if err != nil {
return err return err
} }
@ -93,12 +94,12 @@ func (s *composeService) Down(ctx context.Context, projectName string, options c
return eg.Wait() 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 { for _, container := range containers {
toStop := container toStop := container
eventName := getContainerProgressName(toStop) eventName := getContainerProgressName(toStop)
w.Event(progress.StoppingEvent(eventName)) w.Event(progress.StoppingEvent(eventName))
err := s.apiClient.ContainerStop(ctx, toStop.ID, nil) err := s.apiClient.ContainerStop(ctx, toStop.ID, timeout)
if err != nil { if err != nil {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
return err return err
@ -108,14 +109,14 @@ func (s *composeService) stopContainers(ctx context.Context, w progress.Writer,
return nil 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) eg, _ := errgroup.WithContext(ctx)
for _, container := range containers { for _, container := range containers {
toDelete := container toDelete := container
eg.Go(func() error { eg.Go(func() error {
eventName := getContainerProgressName(toDelete) eventName := getContainerProgressName(toDelete)
w.Event(progress.StoppingEvent(eventName)) 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 { if err != nil {
w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping")) w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
return err return err

View File

@ -20,14 +20,13 @@ import (
"context" "context"
"testing" "testing"
"github.com/golang/mock/gomock" "github.com/docker/compose-cli/api/compose"
"gotest.tools/v3/assert" "github.com/docker/compose-cli/local/mocks"
apitypes "github.com/docker/docker/api/types" apitypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/golang/mock/gomock"
"github.com/docker/compose-cli/api/compose" "gotest.tools/v3/assert"
"github.com/docker/compose-cli/local/mocks"
) )
func TestDown(t *testing.T) { func TestDown(t *testing.T) {

View File

@ -19,16 +19,16 @@ package compose
import ( import (
"context" "context"
"github.com/docker/compose-cli/api/compose"
"github.com/docker/compose-cli/api/progress"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
moby "github.com/docker/docker/api/types" moby "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "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) w := progress.ContextWriter(ctx)
var containers Containers var containers Containers
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
Filters: filters.NewArgs(projectFilter(project.Name)), 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()...)) containers = containers.filter(isService(project.ServiceNames()...))
return InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error { 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)
}) })
} }

View File

@ -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)
}