Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow filtering containers by command #24791

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions docs/source/markdown/podman-ps.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
13 changes: 10 additions & 3 deletions libpod/runtime_ctr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -1259,7 +1266,7 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]*
}
}

return ctrsFiltered, nil
return ctrsFiltered
}

// GetAllContainers is a helper function for GetContainers
Expand Down
5 changes: 5 additions & 0 deletions pkg/domain/entities/container_ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions pkg/domain/entities/types/container_ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
15 changes: 15 additions & 0 deletions pkg/domain/filters/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
45 changes: 40 additions & 5 deletions pkg/ps/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Comment on lines +47 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause regressions, GenerateExternalContainerFilterFuncs() will error on unknown options which means podman ps --external --filter ... will break all other existing filters which does not seem right.

I think design wise it would go long way if we could abstract the same filters for both normal and external container so we do not need the different code handling filters at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause regressions, GenerateExternalContainerFilterFuncs() will error on unknown options which means podman ps --external --filter ... will break all other existing filters which does not seem right.

I think design wise it would go long way if we could abstract the same filters for both normal and external container so we do not need the different code handling filters at all.

I was thinking to implement GenerateFilerFunc as based on generics, however some types which are used in the logic can't be implemented as generics ones.

Do you want me to implement all remaining filters features for external containers?

}
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand All @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/ps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading