mirror of https://github.com/docker/cli.git
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 <github@gone.nl>
(cherry picked from commit 6dfdd1eae9
)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
2fcff17544
commit
fab55e13ce
|
@ -3,28 +3,28 @@ package command
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
eventtypes "github.com/docker/docker/api/types/events"
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EventHandler is abstract interface for user to customize
|
// EventHandler is abstract interface for user to customize
|
||||||
// own handle functions of each type of events
|
// own handle functions of each type of events
|
||||||
type EventHandler interface {
|
type EventHandler interface {
|
||||||
Handle(action string, h func(eventtypes.Message))
|
Handle(action string, h func(events.Message))
|
||||||
Watch(c <-chan eventtypes.Message)
|
Watch(c <-chan events.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitEventHandler initializes and returns an EventHandler
|
// InitEventHandler initializes and returns an EventHandler
|
||||||
func InitEventHandler() 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 {
|
type eventHandler struct {
|
||||||
handlers map[string]func(eventtypes.Message)
|
handlers map[string]func(events.Message)
|
||||||
mu sync.Mutex
|
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.mu.Lock()
|
||||||
w.handlers[action] = h
|
w.handlers[action] = h
|
||||||
w.mu.Unlock()
|
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
|
// Watch ranges over the passed in event chan and processes the events based on the
|
||||||
// handlers created for a given action.
|
// handlers created for a given action.
|
||||||
// To stop watching, close the event chan.
|
// 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 {
|
for e := range c {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
h, exists := w.handlers[e.Action]
|
h, exists := w.handlers[e.Action]
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/events"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ type fakeClient struct {
|
||||||
|
|
||||||
version string
|
version string
|
||||||
serverVersion func(ctx context.Context) (types.Version, error)
|
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) {
|
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 {
|
func (cli *fakeClient) ClientVersion() string {
|
||||||
return cli.version
|
return cli.version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cli *fakeClient) Events(ctx context.Context, opts types.EventsOptions) (<-chan events.Message, <-chan error) {
|
||||||
|
return cli.eventsFn(ctx, opts)
|
||||||
|
}
|
||||||
|
|
|
@ -12,10 +12,12 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"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/opts"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/docker/docker/api/types"
|
"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"
|
"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.since, "since", "", "Show all events created since timestamp")
|
||||||
flags.StringVar(&options.until, "until", "", "Stream events until this timestamp")
|
flags.StringVar(&options.until, "until", "", "Stream events until this timestamp")
|
||||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
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
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -60,21 +62,19 @@ func runEvents(dockerCli command.Cli, options *eventsOptions) error {
|
||||||
Status: "Error parsing format: " + err.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,
|
Since: options.since,
|
||||||
Until: options.until,
|
Until: options.until,
|
||||||
Filters: options.filter.Value(),
|
Filters: options.filter.Value(),
|
||||||
}
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
events, errs := dockerCli.Client().Events(ctx, eventOptions)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
out := dockerCli.Out()
|
out := dockerCli.Out()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-events:
|
case event := <-evts:
|
||||||
if err := handleEvent(out, event, tmpl); err != nil {
|
if err := handleEvent(out, event, tmpl); err != nil {
|
||||||
return err
|
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 {
|
if tmpl == nil {
|
||||||
return prettyPrintEvent(out, event)
|
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) {
|
func makeTemplate(format string) (*template.Template, error) {
|
||||||
if format == "" {
|
switch format {
|
||||||
|
case "":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
case formatter.JSONFormatKey:
|
||||||
|
format = formatter.JSONFormat
|
||||||
}
|
}
|
||||||
tmpl, err := templates.Parse(format)
|
tmpl, err := templates.Parse(format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tmpl, err
|
return tmpl, err
|
||||||
}
|
}
|
||||||
// we execute the template for an empty message, so as to validate
|
// execute the template on an empty message to validate a bad
|
||||||
// a bad template like "{{.badFieldString}}"
|
// template like "{{.badFieldString}}"
|
||||||
return tmpl, tmpl.Execute(io.Discard, &eventtypes.Message{})
|
return tmpl, tmpl.Execute(io.Discard, &events.Message{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds
|
// 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.
|
// prettyPrintEvent prints all types of event information.
|
||||||
// Each output includes the event type, actor id, name and action.
|
// Each output includes the event type, actor id, name and action.
|
||||||
// Actor attributes are printed at the end if the actor has any.
|
// 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 {
|
if event.TimeNano != 0 {
|
||||||
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed))
|
fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed))
|
||||||
} else if event.Time != 0 {
|
} else if event.Time != 0 {
|
||||||
|
@ -141,7 +144,7 @@ func prettyPrintEvent(out io.Writer, event eventtypes.Message) error {
|
||||||
return nil
|
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'})
|
defer out.Write([]byte{'\n'})
|
||||||
return tmpl.Execute(out, event)
|
return tmpl.Execute(out, event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -0,0 +1,4 @@
|
||||||
|
"create"
|
||||||
|
"start"
|
||||||
|
"attach"
|
||||||
|
"die"
|
|
@ -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}
|
|
@ -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}
|
|
@ -9,12 +9,12 @@ Get real time events from the server
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:---------------------------------------|:---------|:--------|:----------------------------------------------|
|
|:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided |
|
| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided |
|
||||||
| [`--format`](#format) | `string` | | Format the output using the given Go template |
|
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>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 |
|
| [`--since`](#since) | `string` | | Show all events created since timestamp |
|
||||||
| `--until` | `string` | | Stream events until this timestamp |
|
| `--until` | `string` | | Stream events until this timestamp |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
@ -401,8 +401,11 @@ Type=container Status=destroy ID=2ee349dac409e97974ce8d01b70d250b85e0ba8189299
|
||||||
|
|
||||||
#### Format as JSON
|
#### Format as JSON
|
||||||
|
|
||||||
|
To list events in JSON format, use the `json` directive, which is the equivalent
|
||||||
|
of `--format '{{ json . }}`.
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker events --format '{{json .}}'
|
$ docker events --format json
|
||||||
|
|
||||||
{"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
{"status":"create","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
||||||
{"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
{"status":"attach","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
||||||
|
@ -410,3 +413,5 @@ $ docker events --format '{{json .}}'
|
||||||
{"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
|
{"status":"start","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f42..
|
||||||
{"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
{"status":"resize","id":"196016a57679bf42424484918746a9474cd905dd993c4d0f4..
|
||||||
```
|
```
|
||||||
|
|
||||||
|
.
|
||||||
|
|
|
@ -9,12 +9,12 @@ Get real time events from the server
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:---------------------------------------|:---------|:--------|:----------------------------------------------|
|
|:---------------------------------------|:---------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided |
|
| [`-f`](#filter), [`--filter`](#filter) | `filter` | | Filter output based on conditions provided |
|
||||||
| [`--format`](#format) | `string` | | Format the output using the given Go template |
|
| [`--format`](#format) | `string` | | Format output using a custom template:<br>'json': Print in JSON format<br>'TEMPLATE': Print output using the given Go template.<br>Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates |
|
||||||
| `--since` | `string` | | Show all events created since timestamp |
|
| `--since` | `string` | | Show all events created since timestamp |
|
||||||
| `--until` | `string` | | Stream events until this timestamp |
|
| `--until` | `string` | | Stream events until this timestamp |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
|
Loading…
Reference in New Issue