Skip to content

Commit

Permalink
add filter for container command
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Krutko <[email protected]>

add a test, improve logic of command filter

Signed-off-by: Oleksandr Krutko <[email protected]>

improve a test

Signed-off-by: Oleksandr Krutko <[email protected]>

improve test, update a man page

Signed-off-by: Oleksandr Krutko <[email protected]>

improve man page, runtime functions

Signed-off-by: Oleksandr Krutko <[email protected]>

move ExternalContainerFilter type to entities package

Signed-off-by: Oleksandr Krutko <[email protected]>

add external filters

Signed-off-by: Oleksandr Krutko <[email protected]>

add tests for external containers

Signed-off-by: Oleksandr Krutko <[email protected]>

add test for ps external id, ancestor

Signed-off-by: Oleksandr Krutko <[email protected]>

add tests for ps external filters of since, before

Signed-off-by: Oleksandr Krutko <[email protected]>

fix linter warnings, add completion for the name filter

Signed-off-by: Oleksandr Krutko <[email protected]>
  • Loading branch information
arsenalzp committed Dec 24, 2024
1 parent 7b35f4f commit 39bf186
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 10 deletions.
49 changes: 48 additions & 1 deletion cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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),
Expand All @@ -107,7 +108,17 @@ func getContainers(cmd *cobra.Command, toComplete string, cType completeType, st
return nil, cobra.ShellCompDirectiveNoFileComp
}

for _, c := range containers {
listContainers = append(listContainers, containers...)

externalContainers, err := engine.ContainerListExternal(registry.GetContext())
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) ||
Expand Down Expand Up @@ -314,6 +325,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 +1704,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
16 changes: 13 additions & 3 deletions libpod/runtime_ctr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -1259,7 +1269,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
28 changes: 28 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,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
}
115 changes: 115 additions & 0 deletions pkg/domain/filters/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
47 changes: 41 additions & 6 deletions pkg/ps/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

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
Loading

0 comments on commit 39bf186

Please sign in to comment.