From 3d9e8fd2d3679c40a3260faff1f6671657db68bd Mon Sep 17 00:00:00 2001 From: ROKUMATE Date: Tue, 30 Jun 2026 18:07:08 +0530 Subject: [PATCH] pkg/domain: return an error instead of nil on remote event parse failure In remote mode `podman events --format json` could print the literal "null" instead of an event object. The remote client converts each event received from the server with ConvertToLibpodEvent, which returned nil when it could not parse the server result (an unknown status or type, or an invalid containerExitCode). The tunnel forwarded that nil as an event with no error set, so the CLI marshalled a nil event and printed "null". Return a descriptive error from ConvertToLibpodEvent and send it on the event channel, which the CLI already handles, instead of forwarding a nil event. Signed-off-by: ROKUMATE --- pkg/domain/entities/events.go | 11 +++-- pkg/domain/entities/events_test.go | 74 ++++++++++++++++++++++++++++++ pkg/domain/infra/tunnel/events.go | 7 ++- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 pkg/domain/entities/events_test.go diff --git a/pkg/domain/entities/events.go b/pkg/domain/entities/events.go index 3cba2490e2..1541e3189e 100644 --- a/pkg/domain/entities/events.go +++ b/pkg/domain/entities/events.go @@ -1,6 +1,7 @@ package entities import ( + "fmt" "strconv" "time" @@ -12,22 +13,22 @@ import ( type Event = types.Event // ConvertToLibpodEvent converts an entities event to a libpod one. -func ConvertToLibpodEvent(e Event) *libpodEvents.Event { +func ConvertToLibpodEvent(e Event) (*libpodEvents.Event, error) { var exitCode int if ec, ok := e.Actor.Attributes["containerExitCode"]; ok { var err error exitCode, err = strconv.Atoi(ec) if err != nil { - return nil + return nil, fmt.Errorf("parsing containerExitCode %q: %w", ec, err) } } status, err := libpodEvents.StringToStatus(string(e.Action)) if err != nil { - return nil + return nil, err } t, err := libpodEvents.StringToType(string(e.Type)) if err != nil { - return nil + return nil, err } var ( oomKilled bool @@ -71,7 +72,7 @@ func ConvertToLibpodEvent(e Event) *libpodEvents.Event { if hasOOM { newEvent.OOMKilled = &oomKilled } - return newEvent + return newEvent, nil } // ConvertToEntitiesEvent converts a libpod event to an entities one. diff --git a/pkg/domain/entities/events_test.go b/pkg/domain/entities/events_test.go new file mode 100644 index 0000000000..3ca77b24ea --- /dev/null +++ b/pkg/domain/entities/events_test.go @@ -0,0 +1,74 @@ +package entities + +import ( + "testing" + "time" + + dockerEvents "github.com/moby/moby/api/types/events" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + libpodEvents "go.podman.io/podman/v6/libpod/events" +) + +func newTestEvent(typ, action string, attrs map[string]string) Event { + if attrs == nil { + attrs = map[string]string{} + } + return Event{ + Message: dockerEvents.Message{ + Type: dockerEvents.Type(typ), + Action: dockerEvents.Action(action), + Actor: dockerEvents.Actor{ID: "abc123", Attributes: attrs}, + TimeNano: 1700000000000000000, + }, + } +} + +func TestConvertToLibpodEvent(t *testing.T) { + e := newTestEvent("container", "start", map[string]string{ + "image": "alpine", + "name": "c1", + "network": "podman1", + "podId": "pod123", + "containerExitCode": "137", + "custom": "value", + }) + event, err := ConvertToLibpodEvent(e) + require.NoError(t, err) + require.NotNil(t, event) + assert.Equal(t, libpodEvents.Container, event.Type) + assert.Equal(t, libpodEvents.Start, event.Status) + assert.Equal(t, "abc123", event.ID) + assert.Equal(t, "alpine", event.Image) + assert.Equal(t, "c1", event.Name) + assert.Equal(t, "podman1", event.Network) + assert.Equal(t, time.Unix(0, 1700000000000000000), event.Time) + require.NotNil(t, event.ContainerExitCode) + assert.Equal(t, 137, *event.ContainerExitCode) + assert.Equal(t, "pod123", event.Details.PodID) + assert.Equal(t, "value", event.Details.Attributes["custom"]) +} + +// Previously a server result that failed to parse was silently turned into a +// nil event, which surfaced to the user as the literal JSON "null". It must now +// return an error instead. See https://github.com/containers/podman/issues/28325 +func TestConvertToLibpodEventReturnsError(t *testing.T) { + tests := []struct { + name string + typ string + action string + attrs map[string]string + }{ + {name: "unknown status", typ: "container", action: "bogus-status"}, + {name: "empty action", typ: "container", action: ""}, + {name: "empty type", typ: "", action: "start"}, + {name: "invalid containerExitCode", typ: "container", action: "start", attrs: map[string]string{"containerExitCode": "not-a-number"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event, err := ConvertToLibpodEvent(newTestEvent(tt.typ, tt.action, tt.attrs)) + assert.Error(t, err) + assert.Nil(t, event) + }) + } +} diff --git a/pkg/domain/infra/tunnel/events.go b/pkg/domain/infra/tunnel/events.go index c04388be74..0708b67296 100644 --- a/pkg/domain/infra/tunnel/events.go +++ b/pkg/domain/infra/tunnel/events.go @@ -24,7 +24,12 @@ func (ic *ContainerEngine) Events(_ context.Context, opts entities.EventsOptions binChan := make(chan entities.Event) go func() { for e := range binChan { - opts.EventChan <- events.ReadResult{Event: entities.ConvertToLibpodEvent(e)} + event, err := entities.ConvertToLibpodEvent(e) + if err != nil { + opts.EventChan <- events.ReadResult{Error: fmt.Errorf("converting event from server: %w", err)} + continue + } + opts.EventChan <- events.ReadResult{Event: event} } close(opts.EventChan) }()