diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 5819900823f..e7130df1778 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -58,6 +58,7 @@ type upOptions struct { noAttach []string timestamp bool wait bool + log bool waitTimeout int watch bool navigationMenu bool @@ -171,6 +172,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backend 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.log, "log", false, "Run in attached mode and stream service logs") 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,11 +196,25 @@ 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.log { + if !up.wait { + return fmt.Errorf("--log should be combined with --wait") + } + + if up.Detach { + return fmt.Errorf("--detach should not be combined with --log") + } + } + if up.wait { 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 !up.log { + up.Detach = true + } } if create.Build && create.noBuild { return fmt.Errorf("--build and --no-build are incompatible") @@ -297,7 +313,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 @@ -339,6 +354,7 @@ func runUp( OnExit: upOptions.OnExit(), Wait: upOptions.wait, WaitTimeout: timeout, + Log: upOptions.log, Watch: upOptions.watch, Services: services, NavigationMenu: upOptions.navigationMenu && display.Mode != "plain" && dockerCli.In().IsTerminal(), diff --git a/cmd/compose/wait.go b/cmd/compose/wait.go index 9d86fd314cf..0f870716a2a 100644 --- a/cmd/compose/wait.go +++ b/cmd/compose/wait.go @@ -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" ) @@ -34,6 +35,7 @@ type waitOptions struct { services []string downProject bool + log bool } func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { @@ -58,22 +60,27 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe } cmd.Flags().BoolVar(&opts.downProject, "down-project", false, "Drops project when the first container stops") + cmd.Flags().BoolVar(&opts.log, "log", false, "Shows the logs of the service") return cmd } 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{ + + return backend.Wait(ctx, name, consumer, api.WaitOptions{ Services: opts.services, DownProjectOnContainerExit: opts.downProject, + Log: opts.log, + Project: project, }) } diff --git a/pkg/api/api.go b/pkg/api/api.go index aed77af1523..c3c57bcaac8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -135,7 +135,7 @@ type Compose interface { // 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) + Wait(ctx context.Context, projectName string, consumer LogConsumer, options WaitOptions) (int64, error) // Scale manages numbers of container instances running per service Scale(ctx context.Context, project *types.Project, options ScaleOptions) error // Export a service container's filesystem as a tar archive @@ -165,6 +165,8 @@ type WaitOptions struct { Services []string // Executes a down when a container exits DownProjectOnContainerExit bool + Log bool + Project *types.Project } type VizOptions struct { @@ -293,6 +295,7 @@ type StartOptions struct { ExitCodeFrom string // Wait won't return until containers reached the running|healthy state Wait bool + Log bool WaitTimeout time.Duration // Services passed in the command line to be started Services []string diff --git a/pkg/compose/start.go b/pkg/compose/start.go index eeb232fa66d..8097cacbbb9 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -95,6 +95,16 @@ func (s *composeService) start(ctx context.Context, projectName string, options } return err } + + if options.Log { + s.Logs(ctx, projectName, options.Attach, api.LogOptions{ + Project: options.Project, + Services: options.Services, + Follow: false, + Tail: "all", + Timestamps: false, + }) + } } return nil diff --git a/pkg/compose/wait.go b/pkg/compose/wait.go index 30413cb533e..f856f93b3c6 100644 --- a/pkg/compose/wait.go +++ b/pkg/compose/wait.go @@ -25,8 +25,14 @@ 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, + consumer api.LogConsumer, + options api.WaitOptions, +) (int64, error) { containers, err := s.getContainers(ctx, projectName, oneOffInclude, false, options.Services...) + if err != nil { return 0, err } @@ -36,6 +42,15 @@ func (s *composeService) Wait(ctx context.Context, projectName string, options a eg, waitCtx := errgroup.WithContext(ctx) var statusCode int64 + + if options.Log { + s.Logs(ctx, projectName, consumer, api.LogOptions{ + Project: options.Project, + Services: options.Services, + Follow: false, + }) + } + for _, ctr := range containers { eg.Go(func() error { var err error 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..1fe9bb1fdca 100644 --- a/pkg/e2e/fixtures/wait/compose.yaml +++ b/pkg/e2e/fixtures/wait/compose.yaml @@ -8,4 +8,6 @@ services: infinity: image: alpine command: sleep infinity - + hello: + image: alpine + command: sh -c "echo hello" diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 19f07b71960..a51b758cf41 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", "--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", diff --git a/pkg/e2e/wait_test.go b/pkg/e2e/wait_test.go index 171027fd9c6..ea453009b07 100644 --- a/pkg/e2e/wait_test.go +++ b/pkg/e2e/wait_test.go @@ -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`}) + }) +}