Add docker compose wait

Signed-off-by: Ulysses Souza <ulyssessouza@gmail.com>
This commit is contained in:
Ulysses Souza 2023-06-26 15:52:50 +02:00
parent c496c23071
commit edd76bfd70
12 changed files with 310 additions and 1 deletions

View file

@ -84,6 +84,15 @@ type Service interface {
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
// Viz generates a graphviz graph of the project services
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
// Wait blocks until at least one of the services' container exits
Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error)
}
type WaitOptions struct {
// Services passed in the command line to be waited
Services []string
// Executes a down when a container exits
DownProjectOnContainerExit bool
}
type VizOptions struct {

View file

@ -54,6 +54,7 @@ type ServiceProxy struct {
MaxConcurrencyFn func(parallel int)
DryRunModeFn func(ctx context.Context, dryRun bool) (context.Context, error)
VizFn func(ctx context.Context, project *types.Project, options VizOptions) (string, error)
WaitFn func(ctx context.Context, projectName string, options WaitOptions) (int64, error)
interceptors []Interceptor
}
@ -95,6 +96,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
s.MaxConcurrencyFn = service.MaxConcurrency
s.DryRunModeFn = service.DryRunMode
s.VizFn = service.Viz
s.WaitFn = service.Wait
return s
}
@ -325,7 +327,7 @@ func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, servic
return s.WatchFn(ctx, project, services, options)
}
// Viz implements Viz interface
// Viz implements Service interface
func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error) {
if s.VizFn == nil {
return "", ErrNotImplemented
@ -333,6 +335,14 @@ func (s *ServiceProxy) Viz(ctx context.Context, project *types.Project, options
return s.VizFn(ctx, project, options)
}
// Wait implements Service interface
func (s *ServiceProxy) Wait(ctx context.Context, projectName string, options WaitOptions) (int64, error) {
if s.WaitFn == nil {
return 0, ErrNotImplemented
}
return s.WaitFn(ctx, projectName, options)
}
func (s *ServiceProxy) MaxConcurrency(i int) {
s.MaxConcurrencyFn(i)
}

67
pkg/compose/wait.go Normal file
View file

@ -0,0 +1,67 @@
/*
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"
"fmt"
"github.com/docker/compose/v2/pkg/api"
"golang.org/x/sync/errgroup"
)
func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...)
if err != nil {
return 0, err
}
if len(containers) == 0 {
return 0, fmt.Errorf("no containers for project %q", projectName)
}
eg, waitCtx := errgroup.WithContext(ctx)
var statusCode int64
for _, c := range containers {
c := c
eg.Go(func() error {
var err error
resultC, errC := s.dockerCli.Client().ContainerWait(waitCtx, c.ID, "")
select {
case result := <-resultC:
fmt.Fprintf(s.dockerCli.Out(), "container %q exited with status code %d\n", c.ID, result.StatusCode)
statusCode = result.StatusCode
case err = <-errC:
}
return err
})
}
err = eg.Wait()
if err != nil {
return 42, err // Ignore abort flag in case of error in wait
}
if options.DownProjectOnContainerExit {
return statusCode, s.Down(ctx, projectName, api.DownOptions{
RemoveOrphans: true,
})
}
return statusCode, err
}

View file

@ -0,0 +1,11 @@
services:
faster:
image: alpine
command: sleep 2
slower:
image: alpine
command: sleep 5
infinity:
image: alpine
command: sleep infinity

72
pkg/e2e/wait_test.go Normal file
View file

@ -0,0 +1,72 @@
/*
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"
"time"
"gotest.tools/v3/assert"
)
func TestWaitOnFaster(t *testing.T) {
const projectName = "e2e-wait-faster"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "faster")
}
func TestWaitOnSlower(t *testing.T) {
const projectName = "e2e-wait-slower"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "slower")
}
func TestWaitOnInfinity(t *testing.T) {
const projectName = "e2e-wait-infinity"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
finished := make(chan struct{})
ticker := time.NewTicker(7 * time.Second)
go func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "infinity")
finished <- struct{}{}
}()
select {
case <-finished:
t.Fatal("wait infinity should not finish")
case <-ticker.C:
}
}
func TestWaitAndDrop(t *testing.T) {
const projectName = "e2e-wait-and-drop"
c := NewParallelCLI(t)
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d")
c.RunDockerComposeCmd(t, "--project-name", projectName, "wait", "--down-project", "faster")
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
}

View file

@ -423,6 +423,21 @@ func (mr *MockServiceMockRecorder) Viz(ctx, project, options interface{}) *gomoc
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Viz", reflect.TypeOf((*MockService)(nil).Viz), ctx, project, options)
}
// Wait mocks base method.
func (m *MockService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait", ctx, projectName, options)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Wait indicates an expected call of Wait.
func (mr *MockServiceMockRecorder) Wait(ctx, projectName, options interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockService)(nil).Wait), ctx, projectName, options)
}
// Watch mocks base method.
func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
m.ctrl.T.Helper()