Merge pull request #1303 from docker/down_timeout

introduce timeout flag on down and stop
This commit is contained in:
Nicolas De loof 2021-02-16 10:59:02 +01:00 committed by GitHub
commit bd9248d074
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
}
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
}

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

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`
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
}

View File

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

View File

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

View File

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

View File

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

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