From 6dfdd1eae9e7bd1a44167fb541224e22d623570c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 28 Aug 2023 01:28:11 +0200 Subject: [PATCH] cli/command/system: fix "docker events" not supporting --format=json Before this patch: docker events --format=json json json json ^C With this patch: docker events --format=json {"status":"create","id":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","from":"hello-world","Type":"container","Action":"create","Actor":{"ID":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","Attributes":{"image":"hello-world","name":"dreamy_goldstine"}},"scope":"local","time":1693168508,"timeNano":1693168508190136885} {"status":"attach","id":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","from":"hello-world","Type":"container","Action":"attach","Actor":{"ID":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","Attributes":{"image":"hello-world","name":"dreamy_goldstine"}},"scope":"local","time":1693168508,"timeNano":1693168508192851593} {"Type":"network","Action":"connect","Actor":{"ID":"c54920dd5074a73e28bea62007e0334d81cc040a90372be311cf16806403d350","Attributes":{"container":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","name":"bridge","type":"bridge"}},"scope":"local","time":1693168508,"timeNano":1693168508212398802} {"status":"start","id":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","from":"hello-world","Type":"container","Action":"start","Actor":{"ID":"4ac3bba8abd68961e627540fed81ad16d55b88e45629d7cdb792126d09b6488d","Attributes":{"image":"hello-world","name":"dreamy_goldstine"}},"scope":"local","time":1693168508,"timeNano":1693168508312969843} ^C Signed-off-by: Sebastiaan van Stijn --- cli/command/events_utils.go | 14 ++-- cli/command/system/client_test.go | 6 ++ cli/command/system/events.go | 33 ++++---- cli/command/system/events_test.go | 83 +++++++++++++++++++ .../testdata/docker-events-default.golden | 4 + .../testdata/docker-events-json-action.golden | 4 + .../docker-events-json-template.golden | 4 + .../system/testdata/docker-events-json.golden | 4 + docs/reference/commandline/events.md | 19 +++-- docs/reference/commandline/system_events.md | 12 +-- 10 files changed, 148 insertions(+), 35 deletions(-) create mode 100644 cli/command/system/events_test.go create mode 100644 cli/command/system/testdata/docker-events-default.golden create mode 100644 cli/command/system/testdata/docker-events-json-action.golden create mode 100644 cli/command/system/testdata/docker-events-json-template.golden create mode 100644 cli/command/system/testdata/docker-events-json.golden diff --git a/cli/command/events_utils.go b/cli/command/events_utils.go index 16d76892a1..6a2907c531 100644 --- a/cli/command/events_utils.go +++ b/cli/command/events_utils.go @@ -3,28 +3,28 @@ package command import ( "sync" - eventtypes "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/events" "github.com/sirupsen/logrus" ) // EventHandler is abstract interface for user to customize // own handle functions of each type of events type EventHandler interface { - Handle(action string, h func(eventtypes.Message)) - Watch(c <-chan eventtypes.Message) + Handle(action string, h func(events.Message)) + Watch(c <-chan events.Message) } // InitEventHandler initializes and returns an EventHandler func InitEventHandler() EventHandler { - return &eventHandler{handlers: make(map[string]func(eventtypes.Message))} + return &eventHandler{handlers: make(map[string]func(events.Message))} } type eventHandler struct { - handlers map[string]func(eventtypes.Message) + handlers map[string]func(events.Message) mu sync.Mutex } -func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { +func (w *eventHandler) Handle(action string, h func(events.Message)) { w.mu.Lock() w.handlers[action] = h w.mu.Unlock() @@ -33,7 +33,7 @@ func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { // Watch ranges over the passed in event chan and processes the events based on the // handlers created for a given action. // To stop watching, close the event chan. -func (w *eventHandler) Watch(c <-chan eventtypes.Message) { +func (w *eventHandler) Watch(c <-chan events.Message) { for e := range c { w.mu.Lock() h, exists := w.handlers[e.Action] diff --git a/cli/command/system/client_test.go b/cli/command/system/client_test.go index 20d8dc38cc..a275426fe1 100644 --- a/cli/command/system/client_test.go +++ b/cli/command/system/client_test.go @@ -4,6 +4,7 @@ import ( "context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" "github.com/docker/docker/client" ) @@ -12,6 +13,7 @@ type fakeClient struct { version string serverVersion func(ctx context.Context) (types.Version, error) + eventsFn func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error) } func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { @@ -21,3 +23,7 @@ func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) func (cli *fakeClient) ClientVersion() string { return cli.version } + +func (cli *fakeClient) Events(ctx context.Context, opts types.EventsOptions) (<-chan events.Message, <-chan error) { + return cli.eventsFn(ctx, opts) +} diff --git a/cli/command/system/events.go b/cli/command/system/events.go index 08b0e47992..d018b4d083 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -12,10 +12,12 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/formatter" + flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/docker/cli/templates" "github.com/docker/docker/api/types" - eventtypes "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/events" "github.com/spf13/cobra" ) @@ -47,7 +49,7 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.since, "since", "", "Show all events created since timestamp") flags.StringVar(&options.until, "until", "", "Stream events until this timestamp") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&options.format, "format", "", "Format the output using the given Go template") + flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now. return cmd } @@ -60,21 +62,19 @@ func runEvents(dockerCli command.Cli, options *eventsOptions) error { Status: "Error parsing format: " + err.Error(), } } - eventOptions := types.EventsOptions{ + ctx, cancel := context.WithCancel(context.Background()) + evts, errs := dockerCli.Client().Events(ctx, types.EventsOptions{ Since: options.since, Until: options.until, Filters: options.filter.Value(), - } - - ctx, cancel := context.WithCancel(context.Background()) - events, errs := dockerCli.Client().Events(ctx, eventOptions) + }) defer cancel() out := dockerCli.Out() for { select { - case event := <-events: + case event := <-evts: if err := handleEvent(out, event, tmpl); err != nil { return err } @@ -87,7 +87,7 @@ func runEvents(dockerCli command.Cli, options *eventsOptions) error { } } -func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { +func handleEvent(out io.Writer, event events.Message, tmpl *template.Template) error { if tmpl == nil { return prettyPrintEvent(out, event) } @@ -96,16 +96,19 @@ func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Templat } func makeTemplate(format string) (*template.Template, error) { - if format == "" { + switch format { + case "": return nil, nil + case formatter.JSONFormatKey: + format = formatter.JSONFormat } tmpl, err := templates.Parse(format) if err != nil { return tmpl, err } - // we execute the template for an empty message, so as to validate - // a bad template like "{{.badFieldString}}" - return tmpl, tmpl.Execute(io.Discard, &eventtypes.Message{}) + // execute the template on an empty message to validate a bad + // template like "{{.badFieldString}}" + return tmpl, tmpl.Execute(io.Discard, &events.Message{}) } // rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds @@ -115,7 +118,7 @@ const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" // prettyPrintEvent prints all types of event information. // Each output includes the event type, actor id, name and action. // Actor attributes are printed at the end if the actor has any. -func prettyPrintEvent(out io.Writer, event eventtypes.Message) error { +func prettyPrintEvent(out io.Writer, event events.Message) error { if event.TimeNano != 0 { fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed)) } else if event.Time != 0 { @@ -141,7 +144,7 @@ func prettyPrintEvent(out io.Writer, event eventtypes.Message) error { return nil } -func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { +func formatEvent(out io.Writer, event events.Message, tmpl *template.Template) error { defer out.Write([]byte{'\n'}) return tmpl.Execute(out, event) } diff --git a/cli/command/system/events_test.go b/cli/command/system/events_test.go new file mode 100644 index 0000000000..1112edd895 --- /dev/null +++ b/cli/command/system/events_test.go @@ -0,0 +1,83 @@ +package system + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" + "gotest.tools/v3/assert" + "gotest.tools/v3/golden" +) + +func TestEventsFormat(t *testing.T) { + var evts []events.Message + for i, action := range []string{"create", "start", "attach", "die"} { + evts = append(evts, events.Message{ + Status: action, + ID: "abc123", + From: "ubuntu:latest", + Type: events.ContainerEventType, + Action: action, + Actor: events.Actor{ + ID: "abc123", + Attributes: map[string]string{"image": "ubuntu:latest"}, + }, + Scope: "local", + Time: int64(time.Second) * int64(i+1), + TimeNano: int64(time.Second) * int64(i+1), + }) + } + tests := []struct { + name, format string + }{ + { + name: "default", + }, + { + name: "json", + format: "json", + }, + { + name: "json template", + format: "{{ json . }}", + }, + { + name: "json action", + format: "{{ json .Action }}", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + // Set to UTC timezone as timestamps in output are + // printed in the current timezone + t.Setenv("TZ", "UTC") + cli := test.NewFakeCli(&fakeClient{eventsFn: func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error) { + messages := make(chan events.Message) + errs := make(chan error, 1) + go func() { + for _, msg := range evts { + messages <- msg + } + errs <- io.EOF + }() + return messages, errs + }}) + cmd := NewEventsCommand(cli) + if tc.format != "" { + cmd.Flags().Set("format", tc.format) + } + assert.Check(t, cmd.Execute()) + out := cli.OutBuffer().String() + assert.Check(t, golden.String(out, fmt.Sprintf("docker-events-%s.golden", strings.ReplaceAll(tc.name, " ", "-")))) + cli.OutBuffer().Reset() + }) + } +} diff --git a/cli/command/system/testdata/docker-events-default.golden b/cli/command/system/testdata/docker-events-default.golden new file mode 100644 index 0000000000..931a7e79c3 --- /dev/null +++ b/cli/command/system/testdata/docker-events-default.golden @@ -0,0 +1,4 @@ +1970-01-01T00:00:01.000000000Z container create abc123 (image=ubuntu:latest) +1970-01-01T00:00:02.000000000Z container start abc123 (image=ubuntu:latest) +1970-01-01T00:00:03.000000000Z container attach abc123 (image=ubuntu:latest) +1970-01-01T00:00:04.000000000Z container die abc123 (image=ubuntu:latest) diff --git a/cli/command/system/testdata/docker-events-json-action.golden b/cli/command/system/testdata/docker-events-json-action.golden new file mode 100644 index 0000000000..467383ae4b --- /dev/null +++ b/cli/command/system/testdata/docker-events-json-action.golden @@ -0,0 +1,4 @@ +"create" +"start" +"attach" +"die" diff --git a/cli/command/system/testdata/docker-events-json-template.golden b/cli/command/system/testdata/docker-events-json-template.golden new file mode 100644 index 0000000000..ec5343fe68 --- /dev/null +++ b/cli/command/system/testdata/docker-events-json-template.golden @@ -0,0 +1,4 @@ +{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/cli/command/system/testdata/docker-events-json.golden b/cli/command/system/testdata/docker-events-json.golden new file mode 100644 index 0000000000..ec5343fe68 --- /dev/null +++ b/cli/command/system/testdata/docker-events-json.golden @@ -0,0 +1,4 @@ +{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/docs/reference/commandline/events.md b/docs/reference/commandline/events.md index 44baef0449..56ca197596 100644 --- a/docs/reference/commandline/events.md +++ b/docs/reference/commandline/events.md @@ -9,12 +9,12 @@ Get real time events from the server ### Options -| Name | Type | Default | Description | -|:---------------------------------------|:---------|:--------|:----------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | -| [`--format`](#format) | `string` | | Format the output using the given Go template | -| [`--since`](#since) | `string` | | Show all events created since timestamp | -| `--until` | `string` | | Stream events until this timestamp | +| Name | Type | Default | Description | +|:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | +| [`--since`](#since) | `string` | | Show all events created since timestamp | +| `--until` | `string` | | Stream events until this timestamp | @@ -401,8 +401,11 @@ Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299 #### Format as JSON +To list events in JSON format, use the `json` directive, which is the equivalent +of `--format '{{ json . }}`. + ```console -$ docker events --format '{{json .}}' +$ docker events --format json {"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4.. {"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4.. @@ -410,3 +413,5 @@ $ docker events --format '{{json .}}' {"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42.. {"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4.. ``` + +. diff --git a/docs/reference/commandline/system_events.md b/docs/reference/commandline/system_events.md index e580f4b9c8..601d4c6e30 100644 --- a/docs/reference/commandline/system_events.md +++ b/docs/reference/commandline/system_events.md @@ -9,12 +9,12 @@ Get real time events from the server ### Options -| Name | Type | Default | Description | -|:---------------------------------------|:---------|:--------|:----------------------------------------------| -| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | -| [`--format`](#format) | `string` | | Format the output using the given Go template | -| `--since` | `string` | | Show all events created since timestamp | -| `--until` | `string` | | Stream events until this timestamp | +| Name | Type | Default | Description | +|:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided | +| [`--format`](#format) | `string` | | Format output using a custom template:
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | +| `--since` | `string` | | Show all events created since timestamp | +| `--until` | `string` | | Stream events until this timestamp |