Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cmd/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend
flags.StringArrayVar(&up.attach, "attach", []string{}, "Restrict attaching to the specified services. Incompatible with --attach-dependencies.")
flags.StringArrayVar(&up.noAttach, "no-attach", []string{}, "Do not attach (stream logs) to the specified services")
flags.BoolVar(&up.attachDependencies, "attach-dependencies", false, "Automatically attach to log output of dependent services")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies detached mode.")
flags.BoolVar(&up.wait, "wait", false, "Wait for services to be running|healthy. Implies attached mode by default.")
flags.IntVar(&up.waitTimeout, "wait-timeout", 0, "Maximum duration in seconds to wait for the project to be running|healthy")
flags.BoolVarP(&up.watch, "watch", "w", false, "Watch source code and rebuild/refresh containers when files are updated.")
flags.BoolVar(&up.navigationMenu, "menu", false, "Enable interactive shortcuts when running attached. Incompatible with --detach. Can also be enable/disable by setting COMPOSE_MENU environment var.")
Expand All @@ -194,12 +194,14 @@ func validateFlags(up *upOptions, create *createOptions) error {
if up.cascadeStop && up.cascadeFail {
return fmt.Errorf("--abort-on-container-failure cannot be combined with --abort-on-container-exit")
}

if up.wait {
up.Detach = false
if up.attachDependencies || up.cascadeStop || len(up.attach) > 0 {
return fmt.Errorf("--wait cannot be combined with --abort-on-container-exit, --attach or --attach-dependencies")
}
up.Detach = true
}

if create.Build && create.noBuild {
return fmt.Errorf("--build and --no-build are incompatible")
}
Expand Down Expand Up @@ -297,7 +299,6 @@ func runUp(
var attach []string
if !upOptions.Detach {
consumer = formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp)

var attachSet utils.Set[string]
if len(upOptions.attach) != 0 {
// services are passed explicitly with --attach, verify they're valid and then use them as-is
Expand Down
8 changes: 7 additions & 1 deletion cmd/compose/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"

"github.com/docker/compose/v5/cmd/formatter"
"github.com/docker/compose/v5/pkg/api"
"github.com/docker/compose/v5/pkg/compose"
)
Expand All @@ -34,6 +35,7 @@ type waitOptions struct {
services []string

downProject bool
log bool
}

func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
Expand Down Expand Up @@ -63,17 +65,21 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
}

func runWait(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *waitOptions) (int64, error) {
_, name, err := opts.projectOrName(ctx, dockerCli)
project, name, err := opts.projectOrName(ctx, dockerCli)
if err != nil {
return 0, err
}

consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
if err != nil {
return 0, err
}

return backend.Wait(ctx, name, api.WaitOptions{
Services: opts.services,
DownProjectOnContainerExit: opts.downProject,
Consumer: consumer,
Project: project,
})
}
4 changes: 4 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ type WaitOptions struct {
Services []string
// Executes a down when a container exits
DownProjectOnContainerExit bool
Project *types.Project
Consumer LogConsumer
}

type VizOptions struct {
Expand Down Expand Up @@ -742,6 +744,8 @@ const (
ContainerEventRecreated
// ContainerEventExited is a ContainerEvent of type exit. ExitCode is set
ContainerEventExited
// ContainerEventHealthy let consumer know container is healthy
ContainerEventHealthy
// UserCancel user canceled compose up, we are stopping containers
HookEventLog
)
Expand Down
5 changes: 5 additions & 0 deletions pkg/compose/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ func (c *monitor) Start(ctx context.Context) error {
}
containers.Remove(ctr.ID)
}
case events.ActionHealthStatusHealthy:
logrus.Debugf("container %s healthy", ctr.Name)
for _, listener := range c.listeners {
listener(newContainerEvent(event.TimeNano, ctr, api.ContainerEventHealthy))
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions pkg/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
}
monitor.withListener(printer.HandleEvent)

if options.Start.Wait {
monitor.withListener(onHealthy(func(e api.ContainerEvent) {
cancel()
}))
}

var exitCode int
if options.Start.OnExit != api.CascadeIgnore {
once := true
Expand Down Expand Up @@ -301,3 +307,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
}
return err
}

func onHealthy(fn func(api.ContainerEvent)) api.ContainerEventListener {
return func(e api.ContainerEvent) {
if e.Type == api.ContainerEventHealthy {
fn(e)
}
}
}
14 changes: 13 additions & 1 deletion pkg/compose/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ import (
"github.com/docker/compose/v5/pkg/api"
)

func (s *composeService) Wait(ctx context.Context, projectName string, options api.WaitOptions) (int64, error) {
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
}
Expand All @@ -36,6 +41,13 @@ func (s *composeService) Wait(ctx context.Context, projectName string, options a

eg, waitCtx := errgroup.WithContext(ctx)
var statusCode int64

s.Logs(ctx, projectName, options.Consumer, api.LogOptions{
Project: options.Project,
Services: options.Services,
Follow: false,
})

for _, ctr := range containers {
eg.Go(func() error {
var err error
Expand Down
3 changes: 3 additions & 0 deletions pkg/e2e/fixtures/start-stop/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ services:
image: nginx:alpine
another:
image: nginx:alpine
hello:
image: alpine
command: echo please-see-me
4 changes: 3 additions & 1 deletion pkg/e2e/fixtures/wait/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ services:
infinity:
image: alpine
command: sleep infinity

hello:
image: alpine
command: sh -c "echo hello"
18 changes: 18 additions & 0 deletions pkg/e2e/start_stop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import (
"fmt"
"strings"
"testing"
"time"

testify "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
)

func TestStartStop(t *testing.T) {
Expand Down Expand Up @@ -243,6 +245,22 @@ func TestStartStopMultipleServices(t *testing.T) {
}
}

func TestStartLogService(t *testing.T) {
c := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-log-svc",
"COMPOSE_FILE=./fixtures/start-stop/compose.yaml"))

t.Run("run wait log", func(t *testing.T) {
cmd := c.NewDockerComposeCmd(t, "up", "hello", "--wait", "--log")
res := icmd.StartCmd(cmd)
t.Cleanup(func() {
_ = res.Cmd.Process.Kill()
})

poll.WaitOn(t, expectOutput(res, "please-see-me"), poll.WithDelay(1000*time.Millisecond), poll.WithTimeout(2*time.Second))
})
}

func TestStartSingleServiceAndDependency(t *testing.T) {
cli := NewParallelCLI(t, WithEnv(
"COMPOSE_PROJECT_NAME=e2e-start-single-deps",
Expand Down
20 changes: 20 additions & 0 deletions pkg/e2e/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,23 @@ func TestWaitAndDrop(t *testing.T) {
res := c.RunDockerCmd(t, "ps", "--all")
assert.Assert(t, !strings.Contains(res.Combined(), projectName), res.Combined())
}

func TestWaitLog(t *testing.T) {
const projectName = "e2e-wait-and-log"
c := NewParallelCLI(t)

cleanup := func() {
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "--timeout=0", "--remove-orphans")
}
t.Cleanup(cleanup)
cleanup()

t.Run("up", func(t *testing.T) {
c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "up", "-d", "hello")
})

t.Run("logs", func(t *testing.T) {
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/wait/compose.yaml", "--project-name", projectName, "wait", "hello", "--log")
res.Assert(t, icmd.Expected{Out: `hello`})
})
}