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