From a3a1b44c31ddd851f164670aa30d0acff5559131 Mon Sep 17 00:00:00 2001 From: AhmedGrati Date: Sat, 16 Dec 2023 14:06:34 +0100 Subject: [PATCH 1/3] libpod/events: Update event time format and add timeNano Add new event type in cmd/podman to better match the docker format. Signed-off-by: AhmedGrati Signed-off-by: Paul Holzinger --- cmd/podman/system/events.go | 101 +++++++++++++++++++++++- docs/source/markdown/podman-events.1.md | 31 ++++---- test/apiv2/27-containersEvents.at | 2 +- test/e2e/events_test.go | 29 +++++-- test/system/090-events.bats | 14 ++-- 5 files changed, 146 insertions(+), 31 deletions(-) diff --git a/cmd/podman/system/events.go b/cmd/podman/system/events.go index fbda9dc300..7d7feaebec 100644 --- a/cmd/podman/system/events.go +++ b/cmd/podman/system/events.go @@ -2,8 +2,10 @@ package system import ( "context" + jsonencoding "encoding/json" "fmt" "os" + "time" "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/report" @@ -12,6 +14,7 @@ import ( "github.com/containers/podman/v5/cmd/podman/validate" "github.com/containers/podman/v5/libpod/events" "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/storage/pkg/stringid" "github.com/spf13/cobra" ) @@ -49,6 +52,97 @@ var ( noTrunc bool ) +type Event struct { + // containerExitCode is for storing the exit code of a container which can + // be used for "internal" event notification + ContainerExitCode *int `json:",omitempty"` + // ID can be for the container, image, volume, etc + ID string `json:",omitempty"` + // Image used where applicable + Image string `json:",omitempty"` + // Name where applicable + Name string `json:",omitempty"` + // Network is the network name in a network event + Network string `json:"network,omitempty"` + // Status describes the event that occurred + Status events.Status + // Time the event occurred + Time int64 `json:"time,omitempty"` + // timeNano the event occurred in nanoseconds + TimeNano int64 `json:"timeNano,omitempty"` + // Type of event that occurred + Type events.Type + // Health status of the current container + HealthStatus string `json:"health_status,omitempty"` + + events.Details +} + +func newEventFromLibpodEvent(e events.Event) Event { + return Event{ + ContainerExitCode: e.ContainerExitCode, + ID: e.ID, + Image: e.Image, + Name: e.Name, + Network: e.Network, + Status: e.Status, + Time: e.Time.Unix(), + Type: e.Type, + HealthStatus: e.HealthStatus, + Details: e.Details, + TimeNano: e.Time.UnixNano(), + } +} + +func (e *Event) ToJSONString() (string, error) { + b, err := jsonencoding.Marshal(e) + return string(b), err +} + +func (e *Event) ToHumanReadable(truncate bool) string { + if e == nil { + return "" + } + var humanFormat string + id := e.ID + if truncate { + id = stringid.TruncateID(id) + } + + timeUnix := time.Unix(0, e.TimeNano) + + switch e.Type { + case events.Container, events.Pod: + humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s", timeUnix, e.Type, e.Status, id, e.Image, e.Name) + if e.PodID != "" { + humanFormat += fmt.Sprintf(", pod_id=%s", e.PodID) + } + if e.HealthStatus != "" { + humanFormat += fmt.Sprintf(", health_status=%s", e.HealthStatus) + } + // check if the container has labels and add it to the output + if len(e.Attributes) > 0 { + for k, v := range e.Attributes { + humanFormat += fmt.Sprintf(", %s=%s", k, v) + } + } + humanFormat += ")" + case events.Network: + humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", timeUnix, e.Type, e.Status, id, id, e.Network) + case events.Image: + humanFormat = fmt.Sprintf("%s %s %s %s %s", timeUnix, e.Type, e.Status, id, e.Name) + case events.System: + if e.Name != "" { + humanFormat = fmt.Sprintf("%s %s %s %s", timeUnix, e.Type, e.Status, e.Name) + } else { + humanFormat = fmt.Sprintf("%s %s %s", timeUnix, e.Type, e.Status) + } + case events.Volume, events.Machine: + humanFormat = fmt.Sprintf("%s %s %s %s", timeUnix, e.Type, e.Status, e.Name) + } + return humanFormat +} + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Command: systemEventsCommand, @@ -70,7 +164,7 @@ func eventsFlags(cmd *cobra.Command) { formatFlagName := "format" flags.StringVar(&eventFormat, formatFlagName, "", "format the output using a Go template") - _ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&events.Event{})) + _ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&Event{})) flags.BoolVar(&eventOptions.Stream, "stream", true, "stream events and do not exit when returning the last known event") @@ -123,9 +217,10 @@ func eventsCmd(cmd *cobra.Command, _ []string) error { // channel was closed we can exit return nil } + e := newEventFromLibpodEvent(*event) switch { case doJSON: - jsonStr, err := event.ToJSONString() + jsonStr, err := e.ToJSONString() if err != nil { return err } @@ -135,7 +230,7 @@ func eventsCmd(cmd *cobra.Command, _ []string) error { return err } default: - fmt.Println(event.ToHumanReadable(!noTrunc)) + fmt.Println(e.ToHumanReadable(!noTrunc)) } case err := <-errChannel: // only exit in case of an error, diff --git a/docs/source/markdown/podman-events.1.md b/docs/source/markdown/podman-events.1.md index 038f2facc5..9e521c1546 100644 --- a/docs/source/markdown/podman-events.1.md +++ b/docs/source/markdown/podman-events.1.md @@ -104,21 +104,22 @@ In the case where an ID is used, the ID may be in its full or shortened form. T Format the output to JSON Lines or using the given Go template. -| **Placeholder** | **Description** | -|-------------------------|-----------------------------------------------| -| .Attributes ... | created_at, _by, labels, and more (map[]) | -| .ContainerExitCode | Exit code (int) | -| .ContainerInspectData | Payload of the container's inspect | -| .HealthStatus | Health Status (string) | -| .ID | Container ID (full 64-bit SHA) | -| .Image | Name of image being run (string) | -| .Name | Container name (string) | -| .Network | Name of network being used (string) | -| .PodID | ID of pod associated with container, if any | -| .Status | Event status (e.g., create, start, died, ...) | -| .Time ... | Event timestamp (string) | -| .ToHumanReadable *bool* | If true, truncates CID in output | -| .Type | Event type (e.g., image, container, pod, ...) | +| **Placeholder** | **Description** | +|-------------------------|----------------------------------------------- ---| +| .Attributes ... | created_at, _by, labels, and more (map[]) | +| .ContainerExitCode | Exit code (int) | +| .ContainerInspectData | Payload of the container's inspect | +| .HealthStatus | Health Status (string) | +| .ID | Container ID (full 64-bit SHA) | +| .Image | Name of image being run (string) | +| .Name | Container name (string) | +| .Network | Name of network being used (string) | +| .PodID | ID of pod associated with container, if any | +| .Status | Event status (e.g., create, start, died, ...) | +| .Time ... | Event timestamp (string) | +| .TimeNano | Event timestamp with nanosecond precision (int64) | +| .ToHumanReadable *bool* | If true, truncates CID in output | +| .Type | Event type (e.g., image, container, pod, ...) | #### **--help** diff --git a/test/apiv2/27-containersEvents.at b/test/apiv2/27-containersEvents.at index 082cfabf45..e57ee46777 100644 --- a/test/apiv2/27-containersEvents.at +++ b/test/apiv2/27-containersEvents.at @@ -29,7 +29,7 @@ t GET "events?stream=false&since=$START" 200 \ 'select(.status | contains("die")).Actor.Attributes.exitCode=1' t GET "events?stream=false&since=$START&type=remove" 200 \ - 'select(.status| contains("remove")).Action=remove' \ + 'select(.status | contains("remove")).Action=remove' \ 'select(.status | contains("remove")).Actor.Attributes.containerExitCode=1' # vim: filetype=sh diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go index 4fb79ece72..a947be35dd 100644 --- a/test/e2e/events_test.go +++ b/test/e2e/events_test.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/containers/podman/v5/libpod/events" + "github.com/containers/podman/v5/cmd/podman/system" . "github.com/containers/podman/v5/test/utils" "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo/v2" @@ -119,7 +119,10 @@ var _ = Describe("Podman events", func() { }) It("podman events format", func() { - _, ec, _ := podmanTest.RunLsContainer("") + start := time.Now() + ctrName := "testCtr" + _, ec, _ := podmanTest.RunLsContainer(ctrName) + end := time.Now() Expect(ec).To(Equal(0)) test := podmanTest.Podman([]string{"events", "--stream=false", "--format", "json"}) @@ -129,21 +132,37 @@ var _ = Describe("Podman events", func() { jsonArr := test.OutputToStringArray() Expect(test.OutputToStringArray()).ShouldNot(BeEmpty()) - event := events.Event{} + event := system.Event{} err := json.Unmarshal([]byte(jsonArr[0]), &event) Expect(err).ToNot(HaveOccurred()) - test = podmanTest.Podman([]string{"events", "--stream=false", "--format", "{{json.}}"}) + test = podmanTest.Podman([]string{ + "events", + "--stream=false", + "--since", strconv.FormatInt(start.Unix(), 10), + "--filter", fmt.Sprintf("container=%s", ctrName), + "--format", "{{json.}}", + }) + test.WaitWithDefaultTimeout() Expect(test).To(ExitCleanly()) jsonArr = test.OutputToStringArray() Expect(test.OutputToStringArray()).ShouldNot(BeEmpty()) - event = events.Event{} + event = system.Event{} err = json.Unmarshal([]byte(jsonArr[0]), &event) Expect(err).ToNot(HaveOccurred()) + Expect(event.Time).To(BeNumerically(">=", start.Unix())) + Expect(event.Time).To(BeNumerically("<=", end.Unix())) + Expect(event.TimeNano).To(BeNumerically(">=", start.UnixNano())) + Expect(event.TimeNano).To(BeNumerically("<=", end.UnixNano())) + Expect(time.Unix(0, event.TimeNano).Unix()).To(BeEquivalentTo(event.Time)) + + date := time.Unix(0, event.TimeNano).Format("2006-01-02") + Expect(event.ToHumanReadable(false)).To(HavePrefix(date)) + test = podmanTest.Podman([]string{"events", "--stream=false", "--filter=type=container", "--format", "ID: {{.ID}}"}) test.WaitWithDefaultTimeout() Expect(test).To(ExitCleanly()) diff --git a/test/system/090-events.bats b/test/system/090-events.bats index bac98897f2..483ce27d2f 100644 --- a/test/system/090-events.bats +++ b/test/system/090-events.bats @@ -223,9 +223,9 @@ EOF # same amount of events. We checked the contents before. CONTAINERS_CONF_OVERRIDE=$containersConf run_podman events --stream=false --since="2022-03-06T11:26:42.723667984+02:00" --format=json assert "${#lines[@]}" = 52 "Number of events returned" - is "${lines[0]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"Time\":\".*\",\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"begin\"}}" - is "${lines[-2]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"Time\":\".*\",\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"end\"}}" - is "${lines[-1]}" "{\"ID\":\"$ctrID\",\"Image\":\"$IMAGE\",\"Name\":\".*\",\"Status\":\"remove\",\"Time\":\".*\",\"Type\":\"container\",\"Attributes\":{.*}}" + is "${lines[0]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"time\":[0-9]\+,\"timeNano\":[0-9]\+,\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"begin\"}}" + is "${lines[-2]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"time\":[0-9]\+,\"timeNano\":[0-9]\+,\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"end\"}}" + is "${lines[-1]}" "{\"ID\":\"$ctrID\",\"Image\":\"$IMAGE\",\"Name\":\".*\",\"Status\":\"remove\",\"time\":[0-9]\+,\"timeNano\":[0-9]\+,\"Type\":\"container\",\"Attributes\":{.*}}" } @test "events log-file no duplicates" { @@ -292,10 +292,10 @@ EOF # Make sure that the JSON stream looks as expected. That means it has all # events and no duplicates. run cat $eventsJSON - is "${lines[0]}" "{\"Name\":\"busybox\",\"Status\":\"pull\",\"Time\":\"2022-04-06T11:26:42.7236679+02:00\",\"Type\":\"image\",\"Attributes\":null}" - is "${lines[99]}" "{\"Name\":\"busybox\",\"Status\":\"pull\",\"Time\":\"2022-04-06T11:26:42.723667999+02:00\",\"Type\":\"image\",\"Attributes\":null}" - is "${lines[100]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"Time\":\".*\",\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"end\"}}" - is "${lines[103]}" "{\"ID\":\"$ctrID\",\"Image\":\"$IMAGE\",\"Name\":\".*\",\"Status\":\"remove\",\"Time\":\".*\",\"Type\":\"container\",\"Attributes\":{.*}}" + is "${lines[0]}" "{\"Name\":\"busybox\",\"Status\":\"pull\",\"time\":1649237202,\"timeNano\":1649237202723[0-9]\+,\"Type\":\"image\",\"Attributes\":null}" + is "${lines[99]}" "{\"Name\":\"busybox\",\"Status\":\"pull\",\"time\":1649237202,\"timeNano\":1649237202723[0-9]\+,\"Type\":\"image\",\"Attributes\":null}" + is "${lines[100]}" "{\"Name\":\"$eventsFile\",\"Status\":\"log-rotation\",\"time\":[0-9]\+,\"timeNano\":[0-9]\+,\"Type\":\"system\",\"Attributes\":{\"io.podman.event.rotate\":\"end\"}}" + is "${lines[103]}" "{\"ID\":\"$ctrID\",\"Image\":\"$IMAGE\",\"Name\":\".*\",\"Status\":\"remove\",\"time\":[0-9]\+,\"timeNano\":[0-9]\+,\"Type\":\"container\",\"Attributes\":{.*}}" } # Prior to #15633, container labels would not appear in 'die' log events From 8f1cebf96f14b80c672544d7d05f1d64722785fb Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 22 Feb 2024 11:42:27 +0100 Subject: [PATCH 2/3] cmd/podman: remove duplicated event ToHumanReadable() ToHumanReadable() exists twice now, there is no reason for this just call the function on the backend event type is fine as this still has to be used there. It also fixes a bug where the wrong event type was passed to the template which did not match the docs and json output. Signed-off-by: Paul Holzinger --- cmd/podman/system/events.go | 57 +++---------------------- docs/source/markdown/podman-events.1.md | 3 +- test/e2e/events_test.go | 5 +-- 3 files changed, 7 insertions(+), 58 deletions(-) diff --git a/cmd/podman/system/events.go b/cmd/podman/system/events.go index 7d7feaebec..ceb056a0d7 100644 --- a/cmd/podman/system/events.go +++ b/cmd/podman/system/events.go @@ -2,10 +2,8 @@ package system import ( "context" - jsonencoding "encoding/json" "fmt" "os" - "time" "github.com/containers/common/pkg/completion" "github.com/containers/common/pkg/report" @@ -14,7 +12,6 @@ import ( "github.com/containers/podman/v5/cmd/podman/validate" "github.com/containers/podman/v5/libpod/events" "github.com/containers/podman/v5/pkg/domain/entities" - "github.com/containers/storage/pkg/stringid" "github.com/spf13/cobra" ) @@ -78,7 +75,7 @@ type Event struct { events.Details } -func newEventFromLibpodEvent(e events.Event) Event { +func newEventFromLibpodEvent(e *events.Event) Event { return Event{ ContainerExitCode: e.ContainerExitCode, ID: e.ID, @@ -95,54 +92,10 @@ func newEventFromLibpodEvent(e events.Event) Event { } func (e *Event) ToJSONString() (string, error) { - b, err := jsonencoding.Marshal(e) + b, err := json.Marshal(e) return string(b), err } -func (e *Event) ToHumanReadable(truncate bool) string { - if e == nil { - return "" - } - var humanFormat string - id := e.ID - if truncate { - id = stringid.TruncateID(id) - } - - timeUnix := time.Unix(0, e.TimeNano) - - switch e.Type { - case events.Container, events.Pod: - humanFormat = fmt.Sprintf("%s %s %s %s (image=%s, name=%s", timeUnix, e.Type, e.Status, id, e.Image, e.Name) - if e.PodID != "" { - humanFormat += fmt.Sprintf(", pod_id=%s", e.PodID) - } - if e.HealthStatus != "" { - humanFormat += fmt.Sprintf(", health_status=%s", e.HealthStatus) - } - // check if the container has labels and add it to the output - if len(e.Attributes) > 0 { - for k, v := range e.Attributes { - humanFormat += fmt.Sprintf(", %s=%s", k, v) - } - } - humanFormat += ")" - case events.Network: - humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", timeUnix, e.Type, e.Status, id, id, e.Network) - case events.Image: - humanFormat = fmt.Sprintf("%s %s %s %s %s", timeUnix, e.Type, e.Status, id, e.Name) - case events.System: - if e.Name != "" { - humanFormat = fmt.Sprintf("%s %s %s %s", timeUnix, e.Type, e.Status, e.Name) - } else { - humanFormat = fmt.Sprintf("%s %s %s", timeUnix, e.Type, e.Status) - } - case events.Volume, events.Machine: - humanFormat = fmt.Sprintf("%s %s %s %s", timeUnix, e.Type, e.Status, e.Name) - } - return humanFormat -} - func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Command: systemEventsCommand, @@ -217,20 +170,20 @@ func eventsCmd(cmd *cobra.Command, _ []string) error { // channel was closed we can exit return nil } - e := newEventFromLibpodEvent(*event) switch { case doJSON: + e := newEventFromLibpodEvent(event) jsonStr, err := e.ToJSONString() if err != nil { return err } fmt.Println(jsonStr) case cmd.Flags().Changed("format"): - if err := rpt.Execute(event); err != nil { + if err := rpt.Execute(newEventFromLibpodEvent(event)); err != nil { return err } default: - fmt.Println(e.ToHumanReadable(!noTrunc)) + fmt.Println(event.ToHumanReadable(!noTrunc)) } case err := <-errChannel: // only exit in case of an error, diff --git a/docs/source/markdown/podman-events.1.md b/docs/source/markdown/podman-events.1.md index 9e521c1546..0267456848 100644 --- a/docs/source/markdown/podman-events.1.md +++ b/docs/source/markdown/podman-events.1.md @@ -116,9 +116,8 @@ Format the output to JSON Lines or using the given Go template. | .Network | Name of network being used (string) | | .PodID | ID of pod associated with container, if any | | .Status | Event status (e.g., create, start, died, ...) | -| .Time ... | Event timestamp (string) | +| .Time | Event timestamp (string) | | .TimeNano | Event timestamp with nanosecond precision (int64) | -| .ToHumanReadable *bool* | If true, truncates CID in output | | .Type | Event type (e.g., image, container, pod, ...) | #### **--help** diff --git a/test/e2e/events_test.go b/test/e2e/events_test.go index a947be35dd..63ca8f239c 100644 --- a/test/e2e/events_test.go +++ b/test/e2e/events_test.go @@ -141,7 +141,7 @@ var _ = Describe("Podman events", func() { "--stream=false", "--since", strconv.FormatInt(start.Unix(), 10), "--filter", fmt.Sprintf("container=%s", ctrName), - "--format", "{{json.}}", + "--format", "{{json .}}", }) test.WaitWithDefaultTimeout() @@ -160,9 +160,6 @@ var _ = Describe("Podman events", func() { Expect(event.TimeNano).To(BeNumerically("<=", end.UnixNano())) Expect(time.Unix(0, event.TimeNano).Unix()).To(BeEquivalentTo(event.Time)) - date := time.Unix(0, event.TimeNano).Format("2006-01-02") - Expect(event.ToHumanReadable(false)).To(HavePrefix(date)) - test = podmanTest.Podman([]string{"events", "--stream=false", "--filter=type=container", "--format", "ID: {{.ID}}"}) test.WaitWithDefaultTimeout() Expect(test).To(ExitCleanly()) From 4078a44056887e3dfc4c026addf4ce942802e68c Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 22 Feb 2024 13:37:27 +0100 Subject: [PATCH 3/3] libpod/events: remove duplicated Details ID field The ID filed in the Event struct is duplicated for no reason, since the Details struct is directly embedded in the Event the ID filed is basically duplicate on the same level multiple times. Removing this one should be be safe and make no change to the resulting json. Signed-off-by: Paul Holzinger --- libpod/events.go | 3 --- libpod/events/config.go | 2 -- 2 files changed, 5 deletions(-) diff --git a/libpod/events.go b/libpod/events.go index 6e862f3676..1ca42ffd74 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -43,7 +43,6 @@ func (c *Container) newContainerEventWithInspectData(status events.Status, inspe e.Type = events.Container e.Details = events.Details{ - ID: e.ID, PodID: c.PodID(), Attributes: c.Labels(), } @@ -99,7 +98,6 @@ func (c *Container) newContainerExitedEvent(exitCode int32) { e.ContainerExitCode = &intExitCode e.Details = events.Details{ - ID: e.ID, Attributes: c.Labels(), } @@ -121,7 +119,6 @@ func (c *Container) newExecDiedEvent(sessionID string, exitCode int) { e.Attributes["execID"] = sessionID e.Details = events.Details{ - ID: e.ID, Attributes: c.Labels(), } diff --git a/libpod/events/config.go b/libpod/events/config.go index 7b31842097..c67f0daae2 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -48,8 +48,6 @@ type Event struct { // Details describes specifics about certain events, specifically around // container events type Details struct { - // ID is the event ID - ID string // ContainerInspectData includes the payload of the container's inspect // data. Only set when events_container_create_inspect_data is set true // in containers.conf.