From ff12dabebbf61f56c2e8c86a2f08de5c4ef24d3a Mon Sep 17 00:00:00 2001 From: Aftab Ali Date: Fri, 11 Sep 2026 13:09:32 +0530 Subject: [PATCH] events: add missing NETWORK filter to generateEventFilter podman events --filter network= 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 --- cmd/podman/common/completion.go | 3 +- docs/source/markdown/podman-events.1.md | 1 + libpod/events/filters.go | 17 ++++ libpod/events/filters_test.go | 110 ++++++++++++++++++++++++ test/e2e/events_test.go | 20 +++++ test/system/090-events.bats | 21 +++++ 6 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 libpod/events/filters_test.go diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 9c0371ea3d..81d504dfaf 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -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, diff --git a/docs/source/markdown/podman-events.1.md b/docs/source/markdown/podman-events.1.md index c4c2216464..6a763a273d 100644 --- a/docs/source/markdown/podman-events.1.md +++ b/docs/source/markdown/podman-events.1.md @@ -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) | diff --git a/libpod/events/filters.go b/libpod/events/filters.go index 008d1640df..beae9dae99 100644 --- a/libpod/events/filters.go +++ b/libpod/events/filters.go @@ -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 { diff --git a/libpod/events/filters_test.go b/libpod/events/filters_test.go new file mode 100644 index 0000000000..ef92291362 --- /dev/null +++ b/libpod/events/filters_test.go @@ -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) + }) + } +} diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go index 9813e01fa8..a129a7b380 100644 --- a/test/e2e/events_test.go +++ b/test/e2e/events_test.go @@ -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") + }) }) diff --git a/test/system/090-events.bats b/test/system/090-events.bats index e081671033..f8d0c85fd6 100644 --- a/test/system/090-events.bats +++ b/test/system/090-events.bats @@ -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= 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" +}