From 0f9c8de41810a7e9fae213c02747525e8e678072 Mon Sep 17 00:00:00 2001 From: Alessio Greggi Date: Tue, 12 Mar 2024 19:55:28 +0100 Subject: [PATCH] feat(compose_up): add --abort-on-container-exit flag Signed-off-by: Alessio Greggi --- cmd/nerdctl/compose_up.go | 29 +++++++++++------- cmd/nerdctl/compose_up_linux_test.go | 45 ++++++++++++++++++++++++++++ docs/command-reference.md | 3 +- pkg/composer/logs.go | 21 +++++++++---- pkg/composer/up.go | 19 ++++++------ pkg/composer/up_service.go | 12 ++++++-- 6 files changed, 100 insertions(+), 29 deletions(-) diff --git a/cmd/nerdctl/compose_up.go b/cmd/nerdctl/compose_up.go index 4fe07ae11d7..b36e4ede0bb 100644 --- a/cmd/nerdctl/compose_up.go +++ b/cmd/nerdctl/compose_up.go @@ -36,7 +36,8 @@ func newComposeUpCommand() *cobra.Command { SilenceUsage: true, SilenceErrors: true, } - composeUpCommand.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background") + composeUpCommand.Flags().Bool("abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d.") + composeUpCommand.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background. Incompatible with --abort-on-container-exit.") composeUpCommand.Flags().Bool("no-build", false, "Don't build an image, even if it's missing.") composeUpCommand.Flags().Bool("no-color", false, "Produce monochrome output") composeUpCommand.Flags().Bool("no-log-prefix", false, "Don't print prefix in logs") @@ -57,6 +58,13 @@ func composeUpAction(cmd *cobra.Command, services []string) error { if err != nil { return err } + abortOnContainerExit, err := cmd.Flags().GetBool("abort-on-container-exit") + if detach && abortOnContainerExit { + return fmt.Errorf("--abort-on-container-exit flag is incompatible with flag --detach") + } + if err != nil { + return err + } noBuild, err := cmd.Flags().GetBool("no-build") if err != nil { return err @@ -121,15 +129,16 @@ func composeUpAction(cmd *cobra.Command, services []string) error { } uo := composer.UpOptions{ - Detach: detach, - NoBuild: noBuild, - NoColor: noColor, - NoLogPrefix: noLogPrefix, - ForceBuild: build, - IPFS: enableIPFS, - QuietPull: quietPull, - RemoveOrphans: removeOrphans, - Scale: scale, + AbortOnContainerExit: abortOnContainerExit, + Detach: detach, + NoBuild: noBuild, + NoColor: noColor, + NoLogPrefix: noLogPrefix, + ForceBuild: build, + IPFS: enableIPFS, + QuietPull: quietPull, + RemoveOrphans: removeOrphans, + Scale: scale, } return c.Up(ctx, uo, services) } diff --git a/cmd/nerdctl/compose_up_linux_test.go b/cmd/nerdctl/compose_up_linux_test.go index 0580e10b28d..ddbea597ef5 100644 --- a/cmd/nerdctl/compose_up_linux_test.go +++ b/cmd/nerdctl/compose_up_linux_test.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" ) func TestComposeUp(t *testing.T) { @@ -585,3 +586,47 @@ services: psCmd.AssertOutContains(serviceRegular) psCmd.AssertOutNotContains(serviceProfiled) } + +func TestComposeUpAbortOnContainerExit(t *testing.T) { + base := testutil.NewBase(t) + serviceRegular := "regular" + serviceProfiled := "exited" + dockerComposeYAML := fmt.Sprintf(` +services: + %s: + image: %s + ports: + - 8080:80 + %s: + image: %s + entrypoint: /bin/sh -c "exit 1" +`, serviceRegular, testutil.NginxAlpineImage, serviceProfiled, testutil.BusyboxImage) + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + + // here we run 'compose up --abort-on-container-exit' command + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--abort-on-container-exit").AssertExitCode(1) + time.Sleep(3 * time.Second) + psCmd := base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") + + psCmd.AssertOutContains(serviceRegular) + psCmd.AssertOutContains(serviceProfiled) + base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + + // this time we run 'compose up' command without --abort-on-container-exit flag + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() + time.Sleep(3 * time.Second) + psCmd = base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited") + + // this time the regular service should not be listed in the output + psCmd.AssertOutNotContains(serviceRegular) + psCmd.AssertOutContains(serviceProfiled) + base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + + // in this sub-test we are ensuring that flags '-d' and '--abort-on-container-exit' cannot be ran together + c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--abort-on-container-exit") + expected := icmd.Expected{ + ExitCode: 1, + } + c.Assert(expected) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index afe15b42797..dcdde45ec83 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1383,7 +1383,8 @@ Usage: `nerdctl compose up [OPTIONS] [SERVICE...]` Flags: -- :whale: `-d, --detach`: Detached mode: Run containers in the background +- :whale: `--abort-on-container-exit`: Stops all containers if any container was stopped. Incompatible with `-d`. +- :whale: `-d, --detach`: Detached mode: Run containers in the background. Incompatible with `--abort-on-container-exit`. - :whale: `--no-build`: Don't build an image, even if it's missing. - :whale: `--no-color`: Produce monochrome output - :whale: `--no-log-prefix`: Don't print prefix in logs diff --git a/pkg/composer/logs.go b/pkg/composer/logs.go index 21cb4fb5623..d9c0cb93a00 100644 --- a/pkg/composer/logs.go +++ b/pkg/composer/logs.go @@ -18,6 +18,7 @@ package composer import ( "context" + "fmt" "os" "os/exec" "os/signal" @@ -32,11 +33,12 @@ import ( ) type LogsOptions struct { - Follow bool - Timestamps bool - Tail string - NoColor bool - NoLogPrefix bool + AbortOnContainerExit bool + Follow bool + Timestamps bool + Tail string + NoColor bool + NoLogPrefix bool } func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string) error { @@ -133,6 +135,7 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container, signal.Notify(interruptChan, os.Interrupt) logsEOFMap := make(map[string]struct{}) // key: container name + var containerError error selectLoop: for { // Wait for Ctrl-C, or `nerdctl compose down` in another terminal @@ -144,6 +147,12 @@ selectLoop: if lo.Follow { // When `nerdctl logs -f` has exited, we can assume that the container has exited log.G(ctx).Infof("Container %q exited", containerName) + // In case a container has exited and the parameter --abort-on-container-exit, + // we break the loop and set an error, so we can exit the program with 1 + if lo.AbortOnContainerExit { + containerError = fmt.Errorf("container %q exited", containerName) + break selectLoop + } } else { log.G(ctx).Debugf("Logs for container %q reached EOF", containerName) } @@ -167,5 +176,5 @@ selectLoop: } } - return nil + return containerError } diff --git a/pkg/composer/up.go b/pkg/composer/up.go index eb2239476cd..4b6fc48a844 100644 --- a/pkg/composer/up.go +++ b/pkg/composer/up.go @@ -28,15 +28,16 @@ import ( ) type UpOptions struct { - Detach bool - NoBuild bool - NoColor bool - NoLogPrefix bool - ForceBuild bool - IPFS bool - QuietPull bool - RemoveOrphans bool - Scale map[string]uint64 // map of service name to replicas + AbortOnContainerExit bool + Detach bool + NoBuild bool + NoColor bool + NoLogPrefix bool + ForceBuild bool + IPFS bool + QuietPull bool + RemoveOrphans bool + Scale map[string]uint64 // map of service name to replicas } func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) error { diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go index ce4a5d798b3..8838eced927 100644 --- a/pkg/composer/up_service.go +++ b/pkg/composer/up_service.go @@ -75,11 +75,17 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars return nil } + // this is used to stop containers in case --abort-on-container-exit flag is set. + // c.Logs returns an error, so we don't need Ctrl-c to reach the "Stopping containers (forcibly)" + if uo.AbortOnContainerExit { + defer c.stopContainersFromParsedServices(ctx, containers) + } log.G(ctx).Info("Attaching to logs") lo := LogsOptions{ - Follow: true, - NoColor: uo.NoColor, - NoLogPrefix: uo.NoLogPrefix, + AbortOnContainerExit: uo.AbortOnContainerExit, + Follow: true, + NoColor: uo.NoColor, + NoLogPrefix: uo.NoLogPrefix, } if err := c.Logs(ctx, lo, services); err != nil { return err