add filter for container command

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add a test, improve logic of command filter

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

improve a test

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

improve test, update a man page

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

improve man page, runtime functions

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

move ExternalContainerFilter type to entities package

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add external filters

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add tests for external containers

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add test for ps external id, ancestor

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

add tests for ps external filters of since, before

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

fix linter warnings, add completion for the name filter

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

resolve conflicts

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

check command length, filter containers liist by external key

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>

re-write test to remove buildah usage

Signed-off-by: Oleksandr Krutko <alexander.krutko@gmail.com>
This commit is contained in:
Oleksandr Krutko
2024-12-06 02:05:59 +02:00
parent 1e7f810f71
commit b18dcccb28
8 changed files with 393 additions and 11 deletions

View File

@ -88,6 +88,7 @@ func setupImageEngine(cmd *cobra.Command) (entities.ImageEngine, error) {
}
func getContainers(cmd *cobra.Command, toComplete string, cType completeType, statuses ...string) ([]string, cobra.ShellCompDirective) {
var listContainers []entities.ListContainer
suggestions := []string{}
listOpts := entities.ContainerListOptions{
Filters: make(map[string][]string),
@ -109,7 +110,20 @@ func getContainers(cmd *cobra.Command, toComplete string, cType completeType, st
return nil, cobra.ShellCompDirectiveNoFileComp
}
for _, c := range containers {
listContainers = append(listContainers, containers...)
// Add containers from the external storage into complete list
if ok, _ := cmd.Flags().GetBool("external"); ok {
externalContainers, err := engine.ContainerListExternal(registry.Context())
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveNoFileComp
}
listContainers = append(listContainers, externalContainers...)
}
for _, c := range listContainers {
// include ids in suggestions if cType == completeIDs or
// more then 2 chars are typed and cType == completeDefault
if ((len(toComplete) > 1 && cType == completeDefault) ||
@ -341,6 +355,43 @@ func getArtifacts(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellC
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.Context(), lsOpts)
if err != nil {
cobra.CompErrorln(err.Error())
return nil, cobra.ShellCompDirectiveNoFileComp
}
externalContainers, err := engine.ContainerListExternal(registry.Context())
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 is done intentionally
// to exclude command arguments from suggestions (e.g. exclude arguments "-g daemon"
// from "nginx -g daemon" output)
if len(container.Command) > 0 {
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 {
@ -1703,6 +1754,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,

View File

@ -61,7 +61,7 @@ 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*

View File

@ -1246,11 +1246,21 @@ 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 {
if filter == nil {
continue
}
include = include && filter(ctr)
}
@ -1259,7 +1269,7 @@ func (r *Runtime) GetContainers(loadState bool, filters ...ContainerFilter) ([]*
}
}
return ctrsFiltered, nil
return ctrsFiltered
}
// GetAllContainers is a helper function for GetContainers

View File

@ -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

View File

@ -118,3 +118,31 @@ func (l ListContainer) USERNS() string {
func (l ListContainer) UTS() string {
return l.Namespaces.UTS
}
func (l ListContainer) Commands() []string {
return l.Command
}
func (l ListContainer) ContainerID() string {
return l.ID
}
func (l ListContainer) LabelsList() map[string]string {
return l.Labels
}
func (l ListContainer) NamesList() []string {
return l.Names
}
func (l ListContainer) ImageInfo() (string, string) {
return l.ImageID, l.Image
}
func (l ListContainer) CreatedTime() time.Time {
return l.Created
}
func (l ListContainer) StatusInfo() string {
return l.Status
}

View File

@ -14,6 +14,8 @@ 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"
"github.com/containers/storage"
)
// GenerateContainerFilterFuncs return ContainerFilter functions based of filter.
@ -282,6 +284,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 +321,112 @@ 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) {
switch filter {
case "id":
return func(listContainer *types.ListContainer) bool {
return filters.FilterID(listContainer.ContainerID(), filterValues)
}, nil
case "name":
// we only have to match one name
return func(listContainer *types.ListContainer) bool {
namesList := listContainer.NamesList()
for _, f := range filterValues {
f = strings.ReplaceAll(f, "/", "")
if util.StringMatchRegexSlice(f, namesList) {
return true
}
}
return false
}, nil
case "command":
return func(listContainer *types.ListContainer) bool {
return util.StringMatchRegexSlice(listContainer.Commands()[0], filterValues)
}, nil
case "ancestor":
// This needs to refine to match docker
// - ancestor=(<image-name>[:tag]|<image-id>| ⟨image@digest⟩) - containers created from an image or a descendant.
return func(listContainer *types.ListContainer) bool {
for _, filterValue := range filterValues {
rootfsImageID, rootfsImageName := listContainer.ImageInfo()
var imageTag string
var imageNameWithoutTag string
// Compare with ImageID, ImageName
// Will match ImageName if running image has tag latest for other tags exact complete filter must be given
name, tag, hasColon := strings.Cut(rootfsImageName, ":")
if hasColon {
imageNameWithoutTag = name
imageTag = tag
}
if (rootfsImageID == filterValue) ||
util.StringMatchRegexSlice(rootfsImageName, filterValues) ||
(util.StringMatchRegexSlice(imageNameWithoutTag, filterValues) && imageTag == "latest") {
return true
}
}
return false
}, nil
case "before":
var createTime time.Time
var externCons []storage.Container
externCons, err := r.StorageContainers()
if err != nil {
return nil, err
}
for _, filterValue := range filterValues {
for _, ctr := range externCons {
if slices.Contains(ctr.Names, filterValue) {
if createTime.IsZero() || createTime.After(ctr.Created) {
createTime = ctr.Created
}
}
}
}
return func(listContainer *types.ListContainer) bool {
return createTime.After(listContainer.CreatedTime())
}, nil
case "since":
var createTime time.Time
var externCons []storage.Container
externCons, err := r.StorageContainers()
if err != nil {
return nil, err
}
for _, filterValue := range filterValues {
for _, ctr := range externCons {
if slices.Contains(ctr.Names, filterValue) {
if createTime.IsZero() || createTime.After(ctr.Created) {
createTime = ctr.Created
}
}
}
}
return func(listContainer *types.ListContainer) bool {
return createTime.Before(listContainer.CreatedTime())
}, nil
case "until":
until, err := filters.ComputeUntilTimestamp(filterValues)
if err != nil {
return nil, err
}
return func(listContainer *types.ListContainer) bool {
if !until.IsZero() && listContainer.CreatedTime().Before(until) {
return true
}
return false
}, nil
case "restart-policy", "network", "pod", "volume", "health", "label", "exited", "status":
return nil, fmt.Errorf("filter %s is not applicable for external containers", filter)
}
return nil, fmt.Errorf("%s is an invalid filter", filter)
}

View File

@ -24,19 +24,33 @@ 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 {
generatedFunc, err := filters.GenerateContainerFilterFuncs(k, v, runtime)
if err != nil {
if err != nil && !options.External {
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"

View File

@ -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()
@ -896,4 +927,110 @@ var _ = Describe("Podman ps", func() {
Expect(session.OutputToString()).To(Or(Equal(net1+","+net2), Equal(net2+","+net1)))
})
// This test checks ps filtering of external container by container command/entrypoint
It("podman ps filter external by container command", func() {
create := podmanTest.Podman([]string{"create", "--name", "test", BB})
create.WaitWithDefaultTimeout()
Expect(create).Should(ExitCleanly())
// Container should exist
Expect(podmanTest.NumberOfContainers()).To(Equal(1))
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "command=sh"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
})
// This test checks ps filtering of external container by container name
It("podman ps filter external by container name", func() {
create := podmanTest.Podman([]string{"create", "--name", "test", BB})
create.WaitWithDefaultTimeout()
Expect(create).Should(ExitCleanly())
// Container should exist
Expect(podmanTest.NumberOfContainers()).To(Equal(1))
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "name=test"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
})
// This test checks ps filtering of external container by container id
It("podman ps filter external by container id", func() {
create := podmanTest.Podman([]string{"create", "--name", "test", BB})
create.WaitWithDefaultTimeout()
Expect(create).Should(ExitCleanly())
// Container should exist
Expect(podmanTest.NumberOfContainers()).To(Equal(1))
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "id=" + create.OutputToString()})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
})
// This test checks ps filtering of external container by container label
It("podman ps filter external by container ancestor", func() {
create := podmanTest.Podman([]string{"create", "--name", "test", BB})
create.WaitWithDefaultTimeout()
Expect(create).Should(ExitCleanly())
// Container should exist
Expect(podmanTest.NumberOfContainers()).To(Equal(1))
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "ancestor=" + BB})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
})
// This test checks ps filtering of external container created earlier than a given
It("podman ps filter external by container created earlier than a given", func() {
early := podmanTest.Podman([]string{"create", "--name", "early", BB})
early.WaitWithDefaultTimeout()
Expect(early).Should(ExitCleanly())
late := podmanTest.Podman([]string{"create", "--name", "late", BB})
late.WaitWithDefaultTimeout()
Expect(late).Should(ExitCleanly())
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "before=late"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
Expect(output).Should(ContainElement(ContainSubstring("early")))
})
// This test checks ps filtering of external container created since a given
It("podman ps filter external by container created since a given", func() {
early := podmanTest.Podman([]string{"create", "--name", "early", BB})
early.WaitWithDefaultTimeout()
Expect(early).Should(ExitCleanly())
late := podmanTest.Podman([]string{"create", "--name", "late", BB})
late.WaitWithDefaultTimeout()
Expect(late).Should(ExitCleanly())
session := podmanTest.Podman([]string{"ps", "-a", "--external", "--no-trunc", "--noheading", "--filter", "since=early"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
output := session.OutputToStringArray()
Expect(output).To(HaveLen(1))
Expect(output).Should(ContainElement(ContainSubstring("late")))
})
})