From 7a8043cf7f5ead850cb3b1d10fe3d40395a2d25c Mon Sep 17 00:00:00 2001 From: Oleksandr Krutko Date: Fri, 6 Dec 2024 02:05:59 +0200 Subject: [PATCH] add filter for container command Signed-off-by: Oleksandr Krutko add a test, improve logic of command filter Signed-off-by: Oleksandr Krutko improve a test Signed-off-by: Oleksandr Krutko --- cmd/podman/common/completion.go | 36 ++++++++++++++++++++++ libpod/runtime_ctr.go | 37 +++++++++++++++++++++-- pkg/domain/entities/types/container_ps.go | 4 +++ pkg/domain/filters/containers.go | 15 +++++++++ pkg/ps/ps.go | 22 +++++++++++--- test/e2e/ps_test.go | 13 ++++++++ 6 files changed, 119 insertions(+), 8 deletions(-) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index ca886b137d..61a481fbb2 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -314,6 +314,41 @@ func getNetworks(cmd *cobra.Command, toComplete string, cType completeType) ([]s return suggestions, cobra.ShellCompDirectiveNoFileComp } +func getCommands(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { + suggestions := []string{} + lsOpts := entities.ContainerListOptions{} + + engine, err := setupContainerEngine(cmd) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + containers, err := engine.ContainerList(registry.GetContext(), lsOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + externalContainers, err := engine.ContainerListExternal(registry.GetContext()) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + containers = append(containers, externalContainers...) + + for _, container := range containers { + // taking of the first element of commands list was done intentionally + // to exclude command arguments from suggestions (e.g. exclude arguments "-g daemon" + // from "nginx -g daemon" output) + if strings.HasPrefix(container.Command[0], toComplete) { + suggestions = append(suggestions, container.Command[0]) + } + } + + return suggestions, cobra.ShellCompDirectiveNoFileComp +} + func fdIsNotDir(f *os.File) bool { stat, err := f.Stat() if err != nil { @@ -1658,6 +1693,7 @@ func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string) kv := keyValueCompletion{ "ancestor=": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) }, "before=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) }, + "command=": func(s string) ([]string, cobra.ShellCompDirective) { return getCommands(cmd, s) }, "exited=": nil, "health=": func(_ string) ([]string, cobra.ShellCompDirective) { return []string{define.HealthCheckHealthy, diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 87d8484783..74c7053a8a 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -21,6 +21,7 @@ import ( "github.com/containers/podman/v5/libpod/define" "github.com/containers/podman/v5/libpod/events" "github.com/containers/podman/v5/libpod/shutdown" + contEntities "github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities/reports" "github.com/containers/podman/v5/pkg/rootless" "github.com/containers/podman/v5/pkg/specgen" @@ -44,6 +45,11 @@ type CtrCreateOption func(*Container) error // A true return will include the container, a false return will exclude it. type ContainerFilter func(*Container) bool +// ExternalContainerFilter is a function to determine whether a container list is included +// in command output. Container lists to be outputted are tested using the function. +// A true return will include the container list, a false return will exclude it. +type ExternalContainerFilter func(*contEntities.ListContainer) bool + // NewContainer creates a new container from a given OCI config. func (r *Runtime) NewContainer(ctx context.Context, rSpec *spec.Spec, spec *specgen.SpecGenerator, infra bool, options ...CtrCreateOption) (*Container, error) { if !r.valid { @@ -1246,9 +1252,16 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]* return nil, err } - ctrsFiltered := make([]*Container, 0, len(ctrs)) + ctrsFiltered := r.ApplyContainersFilters(ctrs, filters...) + + return ctrsFiltered, nil +} + +// Apply container filters on bunch of containers +func (r *Runtime) ApplyContainersFilters(containers []*Container, filters ...ContainerFilter) []*Container { + ctrsFiltered := make([]*Container, 0, len(containers)) - for _, ctr := range ctrs { + for _, ctr := range containers { include := true for _, filter := range filters { include = include && filter(ctr) @@ -1259,7 +1272,25 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]* } } - return ctrsFiltered, nil + return ctrsFiltered +} + +// Apply container filters on bunch of external container lists +func (r *Runtime) ApplyExternalContainersFilters(containersList []*contEntities.ListContainer, filters ...ExternalContainerFilter) []contEntities.ListContainer { + ctrsFiltered := make([]contEntities.ListContainer, 0, len(containersList)) + + for _, ctr := range containersList { + include := true + for _, filter := range filters { + include = include && filter(ctr) + } + + if include { + ctrsFiltered = append(ctrsFiltered, *ctr) + } + } + + return ctrsFiltered } // GetAllContainers is a helper function for GetContainers diff --git a/pkg/domain/entities/types/container_ps.go b/pkg/domain/entities/types/container_ps.go index 139a87c036..abbbeaa3c3 100644 --- a/pkg/domain/entities/types/container_ps.go +++ b/pkg/domain/entities/types/container_ps.go @@ -118,3 +118,7 @@ func (l ListContainer) USERNS() string { func (l ListContainer) UTS() string { return l.Namespaces.UTS } + +func (l ListContainer) Commands() []string { + return l.Command +} diff --git a/pkg/domain/filters/containers.go b/pkg/domain/filters/containers.go index b4a683631c..4cab96963e 100644 --- a/pkg/domain/filters/containers.go +++ b/pkg/domain/filters/containers.go @@ -14,6 +14,7 @@ import ( "github.com/containers/common/pkg/util" "github.com/containers/podman/v5/libpod" "github.com/containers/podman/v5/libpod/define" + "github.com/containers/podman/v5/pkg/domain/entities/types" ) // GenerateContainerFilterFuncs return ContainerFilter functions based of filter. @@ -282,6 +283,10 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo } return false }, filterValueError + case "command": + return func(c *libpod.Container) bool { + return util.StringMatchRegexSlice(c.Command()[0], filterValues) + }, nil } return nil, fmt.Errorf("%s is an invalid filter", filter) } @@ -315,3 +320,13 @@ func prepareUntilFilterFunc(filterValues []string) (func(container *libpod.Conta return false }, nil } + +// GenerateContainerFilterFuncs return ContainerFilter functions based of filter. +func GenerateExternalContainerFilterFuncs(filter string, filterValues []string, r *libpod.Runtime) (func(listContainer *types.ListContainer) bool, error) { + if filter == "command" { + return func(listContainer *types.ListContainer) bool { + return util.StringMatchRegexSlice(listContainer.Commands()[0], filterValues) + }, nil + } + return nil, fmt.Errorf("%s is an invalid filter", filter) +} diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index 7a77621e75..e6b10487d0 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -29,6 +29,7 @@ func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOp pss = []entities.ListContainer{} ) filterFuncs := make([]libpod.ContainerFilter, 0, len(options.Filters)) + filterExtFuncs := make([]libpod.ExternalContainerFilter, 0, len(options.Filters)) all := options.All || options.Last > 0 if len(options.Filters) > 0 { for k, v := range options.Filters { @@ -37,6 +38,14 @@ func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOp return nil, err } filterFuncs = append(filterFuncs, generatedFunc) + + if options.External { + generatedExtFunc, err := filters.GenerateExternalContainerFilterFuncs(k, v, runtime) + if err != nil { + return nil, err + } + filterExtFuncs = append(filterExtFuncs, generatedExtFunc) + } } } @@ -87,7 +96,7 @@ func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOp } if options.External { - listCon, err := GetExternalContainerLists(runtime) + listCon, err := GetExternalContainerLists(runtime, filterExtFuncs...) if err != nil { return nil, err } @@ -107,9 +116,9 @@ func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOp } // GetExternalContainerLists returns list of external containers for e.g. created by buildah -func GetExternalContainerLists(runtime *libpod.Runtime) ([]entities.ListContainer, error) { +func GetExternalContainerLists(runtime *libpod.Runtime, filterExtFuncs ...libpod.ExternalContainerFilter) ([]entities.ListContainer, error) { var ( - pss = []entities.ListContainer{} + pss = []*entities.ListContainer{} ) externCons, err := runtime.StorageContainers() @@ -128,10 +137,13 @@ func GetExternalContainerLists(runtime *libpod.Runtime) ([]entities.ListContaine case err != nil: return nil, err default: - pss = append(pss, listCon) + pss = append(pss, &listCon) } } - return pss, nil + + filteredPss := runtime.ApplyExternalContainersFilters(pss, filterExtFuncs...) + + return filteredPss, nil } // ListContainerBatch is used in ps to reduce performance hits by "batching" diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index c79e1e55e1..46801183a2 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -405,6 +405,19 @@ var _ = Describe("Podman ps", func() { Expect(actual).ToNot(ContainSubstring("NAMES")) }) + It("podman ps filter by container command", func() { + session := podmanTest.Podman([]string{"run", "-d", "--name", "test1", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + result := podmanTest.Podman([]string{"ps", "-a", "--filter", "command=top"}) + result.WaitWithDefaultTimeout() + Expect(result).Should(ExitCleanly()) + + output := result.OutputToStringArray() + Expect(output).To(HaveLen(2)) + }) + It("podman ps mutually exclusive flags", func() { session := podmanTest.Podman([]string{"ps", "-aqs"}) session.WaitWithDefaultTimeout()