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/docs/source/markdown/podman-ps.1.md b/docs/source/markdown/podman-ps.1.md index 0c54d1c1d8..791eda6ed5 100644 --- a/docs/source/markdown/podman-ps.1.md +++ b/docs/source/markdown/podman-ps.1.md @@ -61,6 +61,8 @@ Valid filters are listed below: | pod | [Pod] name or full or partial ID of pod | | network | [Network] name or full ID of network | | until | [DateTime] container created before the given duration or time. | +| command | [Command] the command the container is executing, only argv[0] is taken | + #### **--format**=*format* diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index 87d8484783..79905c3283 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -1246,9 +1246,16 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]* return nil, err } - ctrsFiltered := make([]*Container, 0, len(ctrs)) + ctrsFiltered := applyContainersFilters(ctrs, filters...) - for _, ctr := range ctrs { + return ctrsFiltered, nil +} + +// Applies container filters on bunch of containers +func applyContainersFilters(containers []*Container, filters ...ContainerFilter) []*Container { + ctrsFiltered := make([]*Container, 0, len(containers)) + + for _, ctr := range containers { include := true for _, filter := range filters { include = include && filter(ctr) @@ -1259,7 +1266,7 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]* } } - return ctrsFiltered, nil + return ctrsFiltered } // GetAllContainers is a helper function for GetContainers diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index 0cd8e740f9..b408eec06d 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -8,6 +8,11 @@ import ( "github.com/containers/podman/v5/pkg/domain/entities/types" ) +// 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(*ListContainer) bool + // ListContainer describes a container suitable for listing type ListContainer = types.ListContainer 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..c696eb3955 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -24,11 +24,17 @@ import ( "github.com/sirupsen/logrus" ) +// 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(*entities.ListContainer) bool + func GetContainerLists(runtime *libpod.Runtime, options entities.ContainerListOptions) ([]entities.ListContainer, error) { var ( pss = []entities.ListContainer{} ) filterFuncs := make([]libpod.ContainerFilter, 0, len(options.Filters)) + filterExtFuncs := make([]entities.ExternalContainerFilter, 0, len(options.Filters)) all := options.All || options.Last > 0 if len(options.Filters) > 0 { for k, v := range options.Filters { @@ -37,6 +43,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 +101,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 +121,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 ...entities.ExternalContainerFilter) ([]entities.ListContainer, error) { var ( - pss = []entities.ListContainer{} + pss = []*entities.ListContainer{} ) externCons, err := runtime.StorageContainers() @@ -128,10 +142,31 @@ 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 := applyExternalContainersFilters(pss, filterExtFuncs...) + + return filteredPss, nil +} + +// Apply container filters on bunch of external container lists +func applyExternalContainersFilters(containersList []*entities.ListContainer, filters ...entities.ExternalContainerFilter) []entities.ListContainer { + ctrsFiltered := make([]entities.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 } // 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..d65f4eaf95 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -405,6 +405,37 @@ var _ = Describe("Podman ps", func() { Expect(actual).ToNot(ContainSubstring("NAMES")) }) + // This test checks a ps filtering by container command/entrypoint + // To improve the test reliability a container ID is also checked + It("podman ps filter by container command", func() { + matchedSession := podmanTest.Podman([]string{"run", "-d", "--name", "matched", ALPINE, "top"}) + matchedSession.WaitWithDefaultTimeout() + containedID := matchedSession.OutputToString() // save container ID returned by the run command + Expect(containedID).ShouldNot(BeEmpty()) + Expect(matchedSession).Should(ExitCleanly()) + + matchedSession = podmanTest.Podman([]string{"ps", "-a", "--no-trunc", "--noheading", "--filter", "command=top"}) + matchedSession.WaitWithDefaultTimeout() + Expect(matchedSession).Should(ExitCleanly()) + + output := matchedSession.OutputToStringArray() + Expect(output).To(HaveLen(1)) + Expect(output).Should(ContainElement(ContainSubstring(containedID))) + + unmatchedSession := podmanTest.Podman([]string{"run", "-d", "--name", "unmatched", ALPINE, "sh"}) + unmatchedSession.WaitWithDefaultTimeout() + containedID = unmatchedSession.OutputToString() // save container ID returned by the run command + Expect(containedID).ShouldNot(BeEmpty()) + Expect(unmatchedSession).Should(ExitCleanly()) + + unmatchedSession = podmanTest.Podman([]string{"ps", "-a", "--no-trunc", "--noheading", "--filter", "command=fakecommand"}) + unmatchedSession.WaitWithDefaultTimeout() + Expect(unmatchedSession).Should(ExitCleanly()) + + output = unmatchedSession.OutputToStringArray() + Expect(output).To(BeEmpty()) + }) + It("podman ps mutually exclusive flags", func() { session := podmanTest.Podman([]string{"ps", "-aqs"}) session.WaitWithDefaultTimeout()