diff --git a/cmd/podman/containers/wait.go b/cmd/podman/containers/wait.go index e559c0a1e4..15d19dbd2d 100644 --- a/cmd/podman/containers/wait.go +++ b/cmd/podman/containers/wait.go @@ -56,6 +56,9 @@ func waitFlags(cmd *cobra.Command) { conditionFlagName := "condition" flags.StringSliceVar(&waitOptions.Conditions, conditionFlagName, []string{}, "Condition to wait on") _ = cmd.RegisterFlagCompletionFunc(conditionFlagName, common.AutocompleteWaitCondition) + + waitExitFirst := "exit-first-match" + flags.BoolVar(&waitOptions.ExitFirstMatch, waitExitFirst, false, "Wait for exit of first container which matches conditions, ignore other ones") } func init() { diff --git a/docs/source/markdown/podman-wait.1.md.in b/docs/source/markdown/podman-wait.1.md.in index 0d41ec82cf..ca8c86afb9 100644 --- a/docs/source/markdown/podman-wait.1.md.in +++ b/docs/source/markdown/podman-wait.1.md.in @@ -30,6 +30,9 @@ container to be fully removed. To wait for the removal of a container use #### **--condition**=*state* Container state or condition to wait for. Can be specified multiple times where at least one condition must match for the command to return. Supported values are "configured", "created", "exited", "healthy", "initialized", "paused", "removing", "running", "stopped", "stopping", "unhealthy". The default condition is "stopped". +#### **--exit-first-match** +Wait for exit of first container which matches conditions, ignore other ones. + #### **--help**, **-h** Print usage statement diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 37dfee74be..cb5a662388 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -62,6 +62,8 @@ type WaitOptions struct { Ignore bool // Use the latest created container. Latest bool + // Wait for exit of first container which matches conditions, ignore other ones. + ExitFirstMatch bool } // WaitReport is the result of waiting a container. diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 8b29821132..6b831d2b7b 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -184,6 +184,13 @@ func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []strin if err != nil { return nil, err } + + if options.ExitFirstMatch { + response := waitExitOnFirst(ctx, containers, options) + responses = append(responses, response) + return responses, nil + } + for _, c := range containers { if c.doesNotExist { // Only set when `options.Ignore == true` responses = append(responses, entities.WaitReport{ExitCode: -1}) @@ -208,6 +215,43 @@ func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []strin return responses, nil } +func waitExitOnFirst(ctx context.Context, containers []containerWrapper, options entities.WaitOptions) entities.WaitReport { + var waitChannel = make(chan entities.WaitReport, 1) + var waitFunction = func(ctx context.Context, container containerWrapper, options entities.WaitOptions, waitChannel chan<- entities.WaitReport) { + response := entities.WaitReport{} + var conditions []string + if len(options.Conditions) == 0 { + conditions = []string{define.ContainerStateStopped.String(), define.ContainerStateExited.String()} + } else { + conditions = options.Conditions + } + + exitCode, err := container.WaitForConditionWithInterval(ctx, options.Interval, conditions...) + if err != nil { + response.Error = err + } else { + response.ExitCode = exitCode + } + + select { + case <-ctx.Done(): + case waitChannel <- response: + } + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for _, c := range containers { + if c.doesNotExist { + continue + } + go waitFunction(ctx, c, options, waitChannel) + } + + response := <-waitChannel + return response +} + func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []string, options entities.PauseUnPauseOptions) ([]*entities.PauseUnpauseReport, error) { containers, err := getContainers(ic.Libpod, getContainersOptions{all: options.All, latest: options.Latest, names: namesOrIds, filters: options.Filters}) if err != nil { diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 6372c0adda..4d8c2e6d9a 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -40,6 +40,39 @@ func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrID string, func (ic *ContainerEngine) ContainerWait(ctx context.Context, namesOrIds []string, opts entities.WaitOptions) ([]entities.WaitReport, error) { responses := make([]entities.WaitReport, 0, len(namesOrIds)) options := new(containers.WaitOptions).WithConditions(opts.Conditions).WithInterval(opts.Interval.String()) + + if opts.ExitFirstMatch { + var waitChannel = make(chan entities.WaitReport, 1) + var waitFunction = func(ctx context.Context, nameOrId string, options *containers.WaitOptions, waitChannel chan<- entities.WaitReport) { + response := entities.WaitReport{} + exitCode, err := containers.Wait(ic.ClientCtx, nameOrId, options) + if err != nil { + if opts.Ignore && errorhandling.Contains(err, define.ErrNoSuchCtr) { + response.ExitCode = -1 + } else { + response.Error = err + } + } else { + response.ExitCode = exitCode + } + + select { + case <-ctx.Done(): + case waitChannel <- response: + } + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for _, n := range namesOrIds { + go waitFunction(ctx, n, options, waitChannel) + } + + response := <-waitChannel + responses = append(responses, response) + return responses, nil + } + for _, n := range namesOrIds { response := entities.WaitReport{} exitCode, err := containers.Wait(ic.ClientCtx, n, options) diff --git a/test/e2e/wait_test.go b/test/e2e/wait_test.go index 99dad8583f..4e37b925f0 100644 --- a/test/e2e/wait_test.go +++ b/test/e2e/wait_test.go @@ -123,4 +123,16 @@ var _ = Describe("Podman wait", func() { Expect(session).Should(ExitCleanly()) Expect(session.OutputToString()).To(Equal("-1")) }) + + It("podman wait for first return container", func() { + session1 := podmanTest.PodmanExitCleanly("run", "-d", ALPINE, "sh", "-c", "sleep 100; exit 1") + cid1 := session1.OutputToString() + + session2 := podmanTest.PodmanExitCleanly("run", "-d", ALPINE, "sh", "-c", "sleep 3; exit 2") + cid2 := session2.OutputToString() + + waitSession := podmanTest.PodmanExitCleanly("wait", "--exit-first-match", "--condition", "exited", cid1, cid2) + waitSession.Wait(10) + Expect(waitSession.OutputToString()).To(Equal("2")) + }) })