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) }()