diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 5819900823f..fec28877e0b 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -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.") @@ -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") } @@ -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 diff --git a/pkg/api/api.go b/pkg/api/api.go index aed77af1523..b84029f3a0e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -742,6 +742,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 ) diff --git a/pkg/compose/monitor.go b/pkg/compose/monitor.go index e7e70c88308..b11a2b18d73 100644 --- a/pkg/compose/monitor.go +++ b/pkg/compose/monitor.go @@ -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)) + } } } } diff --git a/pkg/compose/up.go b/pkg/compose/up.go index d5eb4d87691..a1ddbdaa60a 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -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 @@ -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) + } + } +} diff --git a/pkg/e2e/fixtures/start-stop/compose.yaml b/pkg/e2e/fixtures/start-stop/compose.yaml index 15f69b2e305..5d96f26edf9 100644 --- a/pkg/e2e/fixtures/start-stop/compose.yaml +++ b/pkg/e2e/fixtures/start-stop/compose.yaml @@ -3,3 +3,6 @@ services: image: nginx:alpine another: image: nginx:alpine + hello: + image: alpine + command: echo please-see-me diff --git a/pkg/e2e/fixtures/wait/compose.yaml b/pkg/e2e/fixtures/wait/compose.yaml index 1a001e6fa87..92dbe8520a9 100644 --- a/pkg/e2e/fixtures/wait/compose.yaml +++ b/pkg/e2e/fixtures/wait/compose.yaml @@ -7,5 +7,4 @@ services: command: sleep 5 infinity: image: alpine - command: sleep infinity - + command: sleep infinity \ No newline at end of file diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 19f07b71960..b7240fda87d 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -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) { @@ -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") + 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",