Skip to content
Closed
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
14 changes: 13 additions & 1 deletion cmd/podman/parse/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ import (
func FilterArgumentsIntoFilters(filters []string) (url.Values, error) {
parsedFilters := make(url.Values)
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
// Handle negative filters like label!=value by treating them as separate filter keys
var fname, filter string
var hasFilter bool

if strings.Contains(f, "!=") {
fname, filter, hasFilter = strings.Cut(f, "!=")
if hasFilter {
fname += "!"
}
} else {
fname, filter, hasFilter = strings.Cut(f, "=")
}

if !hasFilter {
return parsedFilters, fmt.Errorf("filter input must be in the form of filter=value: %s is invalid", f)
}
Expand Down
4 changes: 2 additions & 2 deletions docs/source/markdown/podman-volume-ls.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ flag. Use the **--quiet** flag to print only the volume names.

Filter what volumes are shown in the output.
Multiple filters can be given with multiple uses of the --filter flag.
Filters with the same key work inclusive, with the only exception being `label`
which is exclusive. Filters with different keys always work exclusive.
Filters with the same key work inclusive (OR logic). Filters with different keys
always work exclusive (AND logic).

Volumes can be filtered by the following attributes:

Expand Down
2 changes: 2 additions & 0 deletions docs/source/markdown/podman-volume-prune.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Provide filter values.

The *filters* argument format is of `key=value`. If there is more than one *filter*, then pass multiple OPTIONS: **--filter** *foo=bar* **--filter** *bif=baz*.

Filters with the same key work inclusive (OR logic). Filters with different keys always work exclusive (AND logic).

Supported filters:

| Filter | Description |
Expand Down
11 changes: 7 additions & 4 deletions libpod/runtime_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func (r *Runtime) HasVolume(name string) (bool, error) {

// Volumes retrieves all volumes
// Filters can be provided which will determine which volumes are included in the
// output. If multiple filters are used, a volume will be returned if
// any of the filters are matched
// output. If multiple filters are used, a volume will be returned only if
// all of the filters are matched
func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) {
if !r.valid {
return nil, define.ErrRuntimeStopped
Expand All @@ -88,9 +88,12 @@ func (r *Runtime) Volumes(filters ...VolumeFilter) ([]*Volume, error) {

volsFiltered := make([]*Volume, 0, len(vols))
for _, vol := range vols {
include := false
include := true
for _, filter := range filters {
include = include || filter(vol)
if !filter(vol) {
include = false
break
}
}

if include {
Expand Down
142 changes: 138 additions & 4 deletions pkg/domain/filters/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,79 @@ func GenerateVolumeFilters(filter string, filterValues []string, runtime *libpod
return false
}, nil
case "label":
// Group filter values by label key to implement correct OR within key, AND across keys logic
labelKeyGroups := make(map[string][]string)
for _, filterValue := range filterValues {
key, value, hasValue := strings.Cut(filterValue, "=")
if !hasValue {
// Handle label key only filters (no value)
labelKeyGroups[key] = append(labelKeyGroups[key], "")
} else {
labelKeyGroups[key] = append(labelKeyGroups[key], value)
}
}

return func(v *libpod.Volume) bool {
return filters.MatchLabelFilters(filterValues, v.Labels())
volumeLabels := v.Labels()
// ALL label keys must match (AND across keys)
for labelKey, values := range labelKeyGroups {
keyMatched := false
// ANY value within the same key can match (OR within key)
for _, value := range values {
if volumeLabel, exists := volumeLabels[labelKey]; exists {
if value == "" || volumeLabel == value {
keyMatched = true
break
}
} else if value == "" {
// Key doesn't exist but we're looking for key existence only
keyMatched = false
break
}
}
if !keyMatched {
return false
}
}
return true
}, nil
case "label!":
// Group filter values by label key for negative matching
labelKeyGroups := make(map[string][]string)
for _, filterValue := range filterValues {
key, value, hasValue := strings.Cut(filterValue, "=")
if !hasValue {
// Handle label key only filters (no value)
labelKeyGroups[key] = append(labelKeyGroups[key], "")
} else {
labelKeyGroups[key] = append(labelKeyGroups[key], value)
}
}

return func(v *libpod.Volume) bool {
return !filters.MatchLabelFilters(filterValues, v.Labels())
volumeLabels := v.Labels()
// ALL label keys must NOT match (AND across keys for negation)
for labelKey, values := range labelKeyGroups {
keyMatched := false
// ANY value within the same key can match (OR within key)
for _, value := range values {
if volumeLabel, exists := volumeLabels[labelKey]; exists {
if value == "" || volumeLabel == value {
keyMatched = true
break
}
} else if value == "" {
// Key doesn't exist but we're looking for key existence only
keyMatched = false
break
}
}
// For negation, if any key matched, the filter fails
if keyMatched {
return false
}
}
return true
}, nil
case "opt":
return func(v *libpod.Volume) bool {
Expand Down Expand Up @@ -101,12 +168,79 @@ func GeneratePruneVolumeFilters(filter string, filterValues []string, runtime *l
case "after", "since":
return createAfterFilterVolumeFunction(filterValues, runtime)
case "label":
// Group filter values by label key to implement correct OR within key, AND across keys logic
labelKeyGroups := make(map[string][]string)
for _, filterValue := range filterValues {
key, value, hasValue := strings.Cut(filterValue, "=")
if !hasValue {
// Handle label key only filters (no value)
labelKeyGroups[key] = append(labelKeyGroups[key], "")
} else {
labelKeyGroups[key] = append(labelKeyGroups[key], value)
}
}

return func(v *libpod.Volume) bool {
return filters.MatchLabelFilters(filterValues, v.Labels())
volumeLabels := v.Labels()
// ALL label keys must match (AND across keys)
for labelKey, values := range labelKeyGroups {
keyMatched := false
// ANY value within the same key can match (OR within key)
for _, value := range values {
if volumeLabel, exists := volumeLabels[labelKey]; exists {
if value == "" || volumeLabel == value {
keyMatched = true
break
}
} else if value == "" {
// Key doesn't exist but we're looking for key existence only
keyMatched = false
break
}
}
if !keyMatched {
return false
}
}
return true
}, nil
case "label!":
// Group filter values by label key for negative matching
labelKeyGroups := make(map[string][]string)
for _, filterValue := range filterValues {
key, value, hasValue := strings.Cut(filterValue, "=")
if !hasValue {
// Handle label key only filters (no value)
labelKeyGroups[key] = append(labelKeyGroups[key], "")
} else {
labelKeyGroups[key] = append(labelKeyGroups[key], value)
}
}

return func(v *libpod.Volume) bool {
return !filters.MatchLabelFilters(filterValues, v.Labels())
volumeLabels := v.Labels()
// ALL label keys must NOT match (AND across keys for negation)
for labelKey, values := range labelKeyGroups {
keyMatched := false
// ANY value within the same key can match (OR within key)
for _, value := range values {
if volumeLabel, exists := volumeLabels[labelKey]; exists {
if value == "" || volumeLabel == value {
keyMatched = true
break
}
} else if value == "" {
// Key doesn't exist but we're looking for key existence only
keyMatched = false
break
}
}
// For negation, if any key matched, the filter fails
if keyMatched {
return false
}
}
return true
}, nil
case "until":
return createUntilFilterVolumeFunction(filterValues)
Expand Down
113 changes: 113 additions & 0 deletions test/e2e/volume_ls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package integration

import (
"fmt"
"time"

. "github.com/containers/podman/v5/test/utils"
. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -216,6 +217,118 @@ var _ = Describe("Podman volume ls", func() {
Expect(session.OutputToStringArray()[0]).To(Equal(vol3Name))
})

It("podman ls volume filters should combine with AND logic", func() {
// Create volumes with different label combinations to test AND logic
session := podmanTest.Podman([]string{"volume", "create", "--label", "a=b", "vol-with-a"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volWithA := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "c=d", "vol-with-c"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volWithC := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "a=b", "--label", "c=d", "vol-with-both"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volWithBoth := session.OutputToString()

// Sleep to ensure time difference for until filter
time.Sleep(1100 * time.Millisecond)
session = podmanTest.Podman([]string{"volume", "create", "--label", "a=b", "vol-new"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volNew := session.OutputToString()

// Test AND logic: both label=a=b AND label=c=d must match
session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=a=b", "--filter", "label=c=d"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// Only vol-with-both should match when filters are combined with AND
Expect(session.OutputToStringArray()).To(HaveLen(1))
Expect(session.OutputToStringArray()[0]).To(Equal(volWithBoth))

// Test AND logic: label=a=b AND label!=c=d must match
session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=a=b", "--filter", "label!=c=d"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// Only volWithA and volNew should match (have a=b but not c=d)
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToStringArray()).To(ContainElement(volWithA))
Expect(session.OutputToStringArray()).To(ContainElement(volNew))

// Test that individual filters still work correctly
session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=a=b"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// Should match all volumes with a=b label
Expect(session.OutputToStringArray()).To(HaveLen(3))
Expect(session.OutputToStringArray()).To(ContainElement(volWithA))
Expect(session.OutputToStringArray()).To(ContainElement(volWithBoth))
Expect(session.OutputToStringArray()).To(ContainElement(volNew))

session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label!=c=d"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// Should match all volumes without c=d label
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToStringArray()).To(ContainElement(volWithA))
Expect(session.OutputToStringArray()).To(ContainElement(volNew))

// Test filtering for volumes with c=d label
session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=c=d"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// Should match volumes with c=d label
Expect(session.OutputToStringArray()).To(HaveLen(2))
Expect(session.OutputToStringArray()).To(ContainElement(volWithC))
Expect(session.OutputToStringArray()).To(ContainElement(volWithBoth))
})

It("podman ls volume filter within-key OR logic combined with cross-key AND logic", func() {
// Test that values within the same filter key use OR, but different keys use AND
session := podmanTest.Podman([]string{"volume", "create", "--label", "env=prod", "vol-prod"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volProd := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "env=dev", "vol-dev"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volDev := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "env=test", "vol-test"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volTest := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "env=prod", "--label", "team=alpha", "vol-prod-alpha"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volProdAlpha := session.OutputToString()

session = podmanTest.Podman([]string{"volume", "create", "--label", "env=dev", "--label", "team=alpha", "vol-dev-alpha"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
volDevAlpha := session.OutputToString()

// Test: env=(prod OR dev) AND team=alpha - should match volumes with team=alpha AND (env=prod OR env=dev)
session = podmanTest.Podman([]string{"volume", "ls", "-q", "--filter", "label=env=prod", "--filter", "label=env=dev", "--filter", "label=team=alpha"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
result := session.OutputToStringArray()

// Should match both volumes that have team=alpha AND have either env=prod OR env=dev
Expect(result).To(HaveLen(2))
Expect(result).To(ContainElement(volProdAlpha))
Expect(result).To(ContainElement(volDevAlpha))
// Should not match volumes without team=alpha, even if they have the right env
Expect(result).NotTo(ContainElement(volProd))
Expect(result).NotTo(ContainElement(volDev))
Expect(result).NotTo(ContainElement(volTest))
})

It("podman ls volume with --filter since/after", func() {
vol1 := "vol1"
vol2 := "vol2"
Expand Down
Loading