events: add missing NETWORK filter to generateEventFilter

podman events --filter network=<name> was unconditionally returning
'NETWORK is an invalid filter' because the NETWORK case was absent
from the generateEventFilter switch statement in filters.go.

All other first-class event types (container, image, pod, volume)
had corresponding filter cases, but the Network type - despite being
fully defined in config.go along with NetworkConnect/NetworkDisconnect
statuses and the Event.Network field - had no handler.

Add the NETWORK case to filter by network name (e.Network), consistent
with Docker's --filter network= behaviour.  ID-prefix matching is
intentionally omitted: for network connect/disconnect events e.ID
holds the container ID, not the network ID, so prefix matching would
only work for create/remove events and silently miss join/leave events.

Also add unit tests (filters_test.go), integration tests
(test/e2e/events_test.go, test/system/090-events.bats), shell
completion support for --filter network= (completion.go), and
document the new filter key in the man page.

Fixes: https://github.com/podman-container-tools/podman/issues/29387
Signed-off-by: Aftab Ali <aftab123215@gmail.com>
This commit is contained in:
Aftab Ali 2026-09-11 13:09:32 +05:30
parent 67aa9604b5
commit ff12dabebb
6 changed files with 171 additions and 1 deletions

View file

@ -1580,7 +1580,7 @@ func getMethodNames(f reflect.Value, prefix string) []formatSuggestion {
}
// AutocompleteEventFilter - Autocomplete event filter flag options.
// -> "container=", "event=", "image=", "pod=", "volume=", "type=", "artifact="
// -> "container=", "event=", "image=", "network=", "pod=", "volume=", "type=", "artifact="
func AutocompleteEventFilter(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
event := func(_ string) ([]string, cobra.ShellCompDirective) {
return []string{
@ -1606,6 +1606,7 @@ func AutocompleteEventFilter(cmd *cobra.Command, _ []string, toComplete string)
"container=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(cmd, s, completeDefault) },
"image=": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(cmd, s) },
"artifact=": func(s string) ([]string, cobra.ShellCompDirective) { return getArtifacts(cmd, s) },
"network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s, completeDefault) },
"pod=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(cmd, s, completeDefault) },
"volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(cmd, s) },
"event=": event,

View file

@ -113,6 +113,7 @@ filters are supported:
| event | event_status (described above) |
| image | [Name or ID] Image name or ID |
| label | [key] or [key=value] label |
| network | [Name] Network name |
| pod | [Name or ID] Pod name or ID |
| volume | [Name or ID] Volume name or ID |
| type | Event_type (described above) |

View file

@ -59,6 +59,23 @@ func generateEventFilter(filter, filterValue string) (func(e *Event) bool, error
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "NETWORK":
return func(e *Event) bool {
if e.Type != Network {
return false
}
if e.Network == filterValue {
return true
}
// For connect/disconnect events e.ID is the container ID, not
// the network ID, so ID-prefix matching would produce false
// positives. Only fall back to HasPrefix for create/remove
// events where e.ID genuinely holds the network ID.
if e.Status == NetworkConnect || e.Status == NetworkDisconnect {
return false
}
return strings.HasPrefix(e.ID, filterValue)
}, nil
case "VOLUME":
return func(e *Event) bool {
if e.Type != Volume {

View file

@ -0,0 +1,110 @@
//go:build linux || freebsd
package events
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestGenerateEventFilter_Network is a regression test for the missing NETWORK
// filter support. Before this fix, calling generateEventFilter("network", ...)
// returned an error ("NETWORK is an invalid filter") instead of a working
// filter function.
func TestGenerateEventFilter_Network(t *testing.T) {
tests := []struct {
name string
filterValue string
event Event
wantMatch bool
}{
{
name: "match by network name",
filterValue: "mynet",
event: Event{Type: Network, Network: "mynet"},
wantMatch: true,
},
{
name: "no match when network name differs",
filterValue: "mynet",
event: Event{Type: Network, Network: "othernet"},
wantMatch: false,
},
{
// For create/remove events e.ID is the network ID, so an
// ID-prefix filter should match.
name: "match by network ID prefix on create event",
filterValue: "abc123",
event: Event{Type: Network, Status: Create, ID: "abc123def456", Network: "mynet"},
wantMatch: true,
},
{
// For connect/disconnect events e.ID is the container ID, not
// the network ID. A filter value that is a prefix of the
// container ID must NOT produce a false positive.
name: "no false-positive on connect event with colliding container ID",
filterValue: "abc123",
event: Event{Type: Network, Status: NetworkConnect, ID: "abc123def456", Network: "mynet"},
wantMatch: false,
},
{
// Same guarantee for disconnect events.
name: "no false-positive on disconnect event with colliding container ID",
filterValue: "abc123",
event: Event{Type: Network, Status: NetworkDisconnect, ID: "abc123def456", Network: "mynet"},
wantMatch: false,
},
{
name: "no match when event type is not Network",
filterValue: "mynet",
event: Event{Type: Container, Network: "mynet"},
wantMatch: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
filter, err := generateEventFilter("network", tc.filterValue)
require.NoError(t, err, "generateEventFilter must not return an error for network filter")
require.NotNil(t, filter)
require.Equal(t, tc.wantMatch, filter(&tc.event))
})
}
}
// TestGenerateEventFilter_InvalidFilter verifies that an unrecognised filter
// key produces the expected error.
func TestGenerateEventFilter_InvalidFilter(t *testing.T) {
_, err := generateEventFilter("doesnotexist", "value")
require.Error(t, err)
require.Contains(t, err.Error(), "invalid filter")
}
// TestGenerateEventFilter_KnownFilters is a smoke-test that all documented
// filter keys are accepted without error.
func TestGenerateEventFilter_KnownFilters(t *testing.T) {
knownFilters := []struct {
key string
value string
}{
{"container", "mycontainer"},
{"event", "start"},
{"status", "stop"},
{"image", "alpine"},
{"artifact", "myartifact"},
{"pod", "mypod"},
{"volume", "myvol"},
{"network", "mynet"},
{"type", "container"},
{"label", "mykey=myvalue"},
}
for _, kf := range knownFilters {
t.Run(kf.key, func(t *testing.T) {
filter, err := generateEventFilter(kf.key, kf.value)
require.NoError(t, err)
require.NotNil(t, filter)
})
}
}

View file

@ -350,4 +350,24 @@ var _ = Describe("Podman events", func() {
Expect(events[2]).To(And(ContainSubstring("artifact remove"), ContainSubstring(artifactName)), "event log includes artifact remove")
Expect(events[3]).To(And(ContainSubstring("artifact pull"), ContainSubstring(artifactName)), "event log includes artifact pull")
})
It("podman events with a network filter", func() {
netName := "net-" + stringid.GenerateRandomID()[:10]
session := podmanTest.Podman([]string{"network", "create", netName})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
rm := podmanTest.Podman([]string{"network", "rm", netName})
rm.WaitWithDefaultTimeout()
Expect(rm).Should(ExitCleanly())
result := podmanTest.Podman([]string{"events", "--stream=false", "--since", "1m", "--filter", "network=" + netName})
result.WaitWithDefaultTimeout()
Expect(result).Should(ExitCleanly())
events := result.OutputToStringArray()
Expect(events).To(HaveLen(2), "number of network events")
Expect(events[0]).To(ContainSubstring("network create"), "first event is network create")
Expect(events[0]).To(ContainSubstring(netName), "create event includes network name")
Expect(events[1]).To(ContainSubstring("network remove"), "second event is network remove")
Expect(events[1]).To(ContainSubstring(netName), "remove event includes network name")
})
})

View file

@ -481,3 +481,24 @@ EOF
run_podman pod rm -f $pname
}
# bats test_tags=ci:parallel
@test "events - network filter" {
local netname=net-$(safename)
# Create and immediately remove a network so we have events to filter.
run_podman network create $netname
run_podman network rm $netname
# --filter network=<name> must return both create and remove events.
run_podman events --since=1m --stream=false --filter network=$netname
assert "${lines[0]}" =~ ".* network create .*name=$netname," \
"network create event returned by --filter network="
assert "${lines[1]}" =~ ".* network remove .*name=$netname," \
"network remove event returned by --filter network="
assert "${#lines[@]}" = "2" "exactly two network events for $netname"
# A different name must not match.
run_podman events --since=1m --stream=false --filter network=nosuchnetwork-$(safename)
assert "$output" = "" "no events for a non-existent network name"
}