From 6d84a9952f1e5be1a187bcc6d9bbc2532331cfc8 Mon Sep 17 00:00:00 2001
From: Karthik Elango <kelango@redhat.com>
Date: Tue, 28 Jun 2022 15:31:20 -0400
Subject: [PATCH] Podman stop --filter flag

Filter flag is added for podman stop and podman --remote stop. Filtering logic is implemented in
getContainersAndInputByContext(). Start filtering can be manipulated to use this logic as well to limit redundancy.

Signed-off-by: Karthik Elango <kelango@redhat.com>
---
 cmd/podman/containers/stop.go              | 17 +++++++--
 cmd/podman/validate/args.go                |  7 ++++
 docs/source/markdown/podman-stop.1.md      | 24 +++++++++++++
 pkg/api/handlers/compat/containers_stop.go |  2 --
 pkg/domain/entities/containers.go          |  1 +
 pkg/domain/infra/abi/containers.go         | 31 ++++++++++++----
 pkg/domain/infra/tunnel/containers.go      |  6 ++--
 pkg/domain/infra/tunnel/helpers.go         | 14 +++++---
 test/e2e/stop_test.go                      | 42 ++++++++++++++++++++++
 9 files changed, 125 insertions(+), 19 deletions(-)

diff --git a/cmd/podman/containers/stop.go b/cmd/podman/containers/stop.go
index 2ddd169a15..261f441c30 100644
--- a/cmd/podman/containers/stop.go
+++ b/cmd/podman/containers/stop.go
@@ -49,7 +49,9 @@ var (
 )
 
 var (
-	stopOptions = entities.StopOptions{}
+	stopOptions = entities.StopOptions{
+		Filters: make(map[string][]string),
+	}
 	stopTimeout uint
 )
 
@@ -67,6 +69,10 @@ func stopFlags(cmd *cobra.Command) {
 	flags.UintVarP(&stopTimeout, timeFlagName, "t", containerConfig.Engine.StopTimeout, "Seconds to wait for stop before killing the container")
 	_ = cmd.RegisterFlagCompletionFunc(timeFlagName, completion.AutocompleteNone)
 
+	filterFlagName := "filter"
+	flags.StringSliceVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
+	_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
+
 	if registry.IsRemote() {
 		_ = flags.MarkHidden("cidfile")
 		_ = flags.MarkHidden("ignore")
@@ -97,7 +103,6 @@ func stop(cmd *cobra.Command, args []string) error {
 	if cmd.Flag("time").Changed {
 		stopOptions.Timeout = &stopTimeout
 	}
-
 	for _, cidFile := range cidFiles {
 		content, err := ioutil.ReadFile(cidFile)
 		if err != nil {
@@ -107,6 +112,14 @@ func stop(cmd *cobra.Command, args []string) error {
 		args = append(args, id)
 	}
 
+	for _, f := range filters {
+		split := strings.SplitN(f, "=", 2)
+		if len(split) < 2 {
+			return fmt.Errorf("invalid filter %q", f)
+		}
+		stopOptions.Filters[split[0]] = append(stopOptions.Filters[split[0]], split[1])
+	}
+
 	responses, err := registry.ContainerEngine().ContainerStop(context.Background(), args, stopOptions)
 	if err != nil {
 		return err
diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go
index 39eedca648..6d212665d3 100644
--- a/cmd/podman/validate/args.go
+++ b/cmd/podman/validate/args.go
@@ -86,6 +86,13 @@ func CheckAllLatestAndIDFile(c *cobra.Command, args []string, ignoreArgLen bool,
 		specifiedIDFile = true
 	}
 
+	if c.Flags().Changed("filter") {
+		if argLen > 0 {
+			return errors.New("--filter takes no arguments")
+		}
+		return nil
+	}
+
 	if specifiedIDFile && (specifiedAll || specifiedLatest) {
 		return fmt.Errorf("--all, --latest, and --%s cannot be used together", idFileFlag)
 	} else if specifiedAll && specifiedLatest {
diff --git a/docs/source/markdown/podman-stop.1.md b/docs/source/markdown/podman-stop.1.md
index e35ab9182e..cfc49afa1f 100644
--- a/docs/source/markdown/podman-stop.1.md
+++ b/docs/source/markdown/podman-stop.1.md
@@ -25,6 +25,30 @@ Stop all running containers.  This does not include paused containers.
 
 Read container ID from the specified file and remove the container.  Can be specified multiple times.
 
+#### **--filter**, **-f**=*filter*
+
+Filter what containers are going to be stopped.
+Multiple filters can be given with multiple uses of the --filter flag.
+Filters with the same key work inclusive with the only exception being
+`label` which is exclusive. Filters with different keys always work exclusive.
+
+Valid filters are listed below:
+
+| **Filter**      | **Description**                                                                  |
+| --------------- | -------------------------------------------------------------------------------- |
+| id              | [ID] Container's ID (accepts regex)                                              |
+| name            | [Name] Container's name (accepts regex)                                          |
+| label           | [Key] or [Key=Value] Label assigned to a container                               |
+| exited          | [Int] Container's exit code                                                      |
+| status          | [Status] Container's status: 'created', 'exited', 'paused', 'running', 'unknown' |
+| ancestor        | [ImageName] Image or descendant used to create container                         |
+| before          | [ID] or [Name] Containers created before this container                          |
+| since           | [ID] or [Name] Containers created since this container                           |
+| volume          | [VolumeName] or [MountpointDestination] Volume mounted in container              |
+| health          | [Status] healthy or unhealthy                                                    |
+| pod             | [Pod] name or full or partial ID of pod                                          |
+| network         | [Network] name or full ID of network                                             |
+
 #### **--ignore**, **-i**
 
 Ignore errors when specified containers are not in the container store.  A user
diff --git a/pkg/api/handlers/compat/containers_stop.go b/pkg/api/handlers/compat/containers_stop.go
index 33bb3a679c..c9a27dd834 100644
--- a/pkg/api/handlers/compat/containers_stop.go
+++ b/pkg/api/handlers/compat/containers_stop.go
@@ -33,9 +33,7 @@ func StopContainer(w http.ResponseWriter, r *http.Request) {
 		utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
 		return
 	}
-
 	name := utils.GetName(r)
-
 	options := entities.StopOptions{
 		Ignore: query.Ignore,
 	}
diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go
index 17408f12fb..934a7cbdc4 100644
--- a/pkg/domain/entities/containers.go
+++ b/pkg/domain/entities/containers.go
@@ -80,6 +80,7 @@ type PauseUnpauseReport struct {
 }
 
 type StopOptions struct {
+	Filters map[string][]string
 	All     bool
 	Ignore  bool
 	Latest  bool
diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go
index 23a5916042..04eb855044 100644
--- a/pkg/domain/infra/abi/containers.go
+++ b/pkg/domain/infra/abi/containers.go
@@ -37,12 +37,29 @@ import (
 )
 
 // getContainersAndInputByContext gets containers whether all, latest, or a slice of names/ids
-// is specified.  It also returns a list of the corresponding input name used to look up each container.
-func getContainersAndInputByContext(all, latest bool, names []string, runtime *libpod.Runtime) (ctrs []*libpod.Container, rawInput []string, err error) {
+// is specified.  It also returns a list of the corresponding input name used to lookup each container.
+func getContainersAndInputByContext(all, latest bool, names []string, filters map[string][]string, runtime *libpod.Runtime) (ctrs []*libpod.Container, rawInput []string, err error) {
 	var ctr *libpod.Container
 	ctrs = []*libpod.Container{}
+	filterFuncs := make([]libpod.ContainerFilter, 0, len(filters))
 
 	switch {
+	case len(filters) > 0:
+		for k, v := range filters {
+			generatedFunc, err := dfilters.GenerateContainerFilterFuncs(k, v, runtime)
+			if err != nil {
+				return nil, nil, err
+			}
+			filterFuncs = append(filterFuncs, generatedFunc)
+		}
+		ctrs, err = runtime.GetContainers(filterFuncs...)
+		if err != nil {
+			return nil, nil, err
+		}
+		rawInput = []string{}
+		for _, candidate := range ctrs {
+			rawInput = append(rawInput, candidate.ID())
+		}
 	case all:
 		ctrs, err = runtime.GetAllContainers()
 	case latest:
@@ -66,13 +83,13 @@ func getContainersAndInputByContext(all, latest bool, names []string, runtime *l
 			}
 		}
 	}
-	return
+	return ctrs, rawInput, err
 }
 
 // getContainersByContext gets containers whether all, latest, or a slice of names/ids
 // is specified.
 func getContainersByContext(all, latest bool, names []string, runtime *libpod.Runtime) (ctrs []*libpod.Container, err error) {
-	ctrs, _, err = getContainersAndInputByContext(all, latest, names, runtime)
+	ctrs, _, err = getContainersAndInputByContext(all, latest, names, nil, runtime)
 	return
 }
 
@@ -150,7 +167,7 @@ func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []st
 }
 func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) {
 	names := namesOrIds
-	ctrs, rawInputs, err := getContainersAndInputByContext(options.All, options.Latest, names, ic.Libpod)
+	ctrs, rawInputs, err := getContainersAndInputByContext(options.All, options.Latest, names, options.Filters, ic.Libpod)
 	if err != nil && !(options.Ignore && errors.Is(err, define.ErrNoSuchCtr)) {
 		return nil, err
 	}
@@ -228,7 +245,7 @@ func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []strin
 	if err != nil {
 		return nil, err
 	}
-	ctrs, rawInputs, err := getContainersAndInputByContext(options.All, options.Latest, namesOrIds, ic.Libpod)
+	ctrs, rawInputs, err := getContainersAndInputByContext(options.All, options.Latest, namesOrIds, nil, ic.Libpod)
 	if err != nil {
 		return nil, err
 	}
@@ -874,7 +891,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri
 			}
 		}
 	}
-	ctrs, rawInputs, err := getContainersAndInputByContext(all, options.Latest, containersNamesOrIds, ic.Libpod)
+	ctrs, rawInputs, err := getContainersAndInputByContext(all, options.Latest, containersNamesOrIds, options.Filters, ic.Libpod)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go
index 5568ccde89..fcabff7c42 100644
--- a/pkg/domain/infra/tunnel/containers.go
+++ b/pkg/domain/infra/tunnel/containers.go
@@ -91,8 +91,7 @@ func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []st
 }
 
 func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, opts entities.StopOptions) ([]*entities.StopReport, error) {
-	reports := []*entities.StopReport{}
-	ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, opts.All, opts.Ignore, namesOrIds)
+	ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, opts.All, opts.Ignore, namesOrIds, opts.Filters)
 	if err != nil {
 		return nil, err
 	}
@@ -104,6 +103,7 @@ func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []strin
 	if to := opts.Timeout; to != nil {
 		options.WithTimeout(*to)
 	}
+	reports := []*entities.StopReport{}
 	for _, c := range ctrs {
 		report := entities.StopReport{
 			Id:       c.ID,
@@ -134,7 +134,7 @@ func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []strin
 }
 
 func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []string, opts entities.KillOptions) ([]*entities.KillReport, error) {
-	ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, opts.All, false, namesOrIds)
+	ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, opts.All, false, namesOrIds, nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/domain/infra/tunnel/helpers.go b/pkg/domain/infra/tunnel/helpers.go
index 24b2b619d0..9ff1641f02 100644
--- a/pkg/domain/infra/tunnel/helpers.go
+++ b/pkg/domain/infra/tunnel/helpers.go
@@ -15,25 +15,29 @@ import (
 // FIXME: the `ignore` parameter is very likely wrong here as it should rather
 //        be used on *errors* from operations such as remove.
 func getContainersByContext(contextWithConnection context.Context, all, ignore bool, namesOrIDs []string) ([]entities.ListContainer, error) {
-	ctrs, _, err := getContainersAndInputByContext(contextWithConnection, all, ignore, namesOrIDs)
+	ctrs, _, err := getContainersAndInputByContext(contextWithConnection, all, ignore, namesOrIDs, nil)
 	return ctrs, err
 }
 
-func getContainersAndInputByContext(contextWithConnection context.Context, all, ignore bool, namesOrIDs []string) ([]entities.ListContainer, []string, error) {
+func getContainersAndInputByContext(contextWithConnection context.Context, all, ignore bool, namesOrIDs []string, filters map[string][]string) ([]entities.ListContainer, []string, error) {
 	if all && len(namesOrIDs) > 0 {
 		return nil, nil, errors.New("cannot look up containers and all")
 	}
-	options := new(containers.ListOptions).WithAll(true).WithSync(true)
+	options := new(containers.ListOptions).WithAll(true).WithSync(true).WithFilters(filters)
 	allContainers, err := containers.List(contextWithConnection, options)
 	if err != nil {
 		return nil, nil, err
 	}
 	rawInputs := []string{}
-	if all {
+	switch {
+	case len(filters) > 0:
+		for i := range allContainers {
+			namesOrIDs = append(namesOrIDs, allContainers[i].ID)
+		}
+	case all:
 		for i := range allContainers {
 			rawInputs = append(rawInputs, allContainers[i].ID)
 		}
-
 		return allContainers, rawInputs, err
 	}
 
diff --git a/test/e2e/stop_test.go b/test/e2e/stop_test.go
index 97d8ba701f..7a258466ab 100644
--- a/test/e2e/stop_test.go
+++ b/test/e2e/stop_test.go
@@ -1,6 +1,7 @@
 package integration
 
 import (
+	"fmt"
 	"io/ioutil"
 	"os"
 	"strings"
@@ -363,4 +364,45 @@ var _ = Describe("Podman stop", func() {
 		Expect(session).Should(Exit(0))
 		Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
 	})
+
+	It("podman stop --filter", func() {
+		session1 := podmanTest.Podman([]string{"container", "create", ALPINE})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		cid1 := session1.OutputToString()
+
+		session1 = podmanTest.Podman([]string{"container", "create", ALPINE})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		cid2 := session1.OutputToString()
+
+		session1 = podmanTest.Podman([]string{"container", "create", ALPINE})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		cid3 := session1.OutputToString()
+		shortCid3 := cid3[0:5]
+
+		session1 = podmanTest.Podman([]string{"start", "--all"})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+
+		session1 = podmanTest.Podman([]string{"stop", cid1, "-f", "status=running"})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(125))
+
+		session1 = podmanTest.Podman([]string{"stop", "-a", "--filter", fmt.Sprintf("id=%swrongid", shortCid3)})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		Expect(session1.OutputToString()).To(HaveLen(0))
+
+		session1 = podmanTest.Podman([]string{"stop", "-a", "--filter", fmt.Sprintf("id=%s", shortCid3)})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		Expect(session1.OutputToString()).To(BeEquivalentTo(cid3))
+
+		session1 = podmanTest.Podman([]string{"stop", "-f", fmt.Sprintf("id=%s", cid2)})
+		session1.WaitWithDefaultTimeout()
+		Expect(session1).Should(Exit(0))
+		Expect(session1.OutputToString()).To(BeEquivalentTo(cid2))
+	})
 })