diff --git a/cli/command/builder/cmd.go b/cli/command/builder/cmd.go index 724f7ca9c984..cb92c63113db 100644 --- a/cli/command/builder/cmd.go +++ b/cli/command/builder/cmd.go @@ -18,6 +18,7 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command { Annotations: map[string]string{"version": "1.31"}, } cmd.AddCommand( + newEventsCommand(dockerCli), NewPruneCommand(dockerCli), image.NewBuildCommand(dockerCli), ) diff --git a/cli/command/builder/events.go b/cli/command/builder/events.go new file mode 100644 index 000000000000..d1d4e1b9a82e --- /dev/null +++ b/cli/command/builder/events.go @@ -0,0 +1,13 @@ +package builder + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker builder events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.BuilderEventType) +} diff --git a/cli/command/config/cmd.go b/cli/command/config/cmd.go index 86ad1cc09cde..9555d974f5ef 100644 --- a/cli/command/config/cmd.go +++ b/cli/command/config/cmd.go @@ -23,6 +23,7 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command { cmd.AddCommand( newConfigListCommand(dockerCli), newConfigCreateCommand(dockerCli), + newConfigEventsCommand(dockerCli), newConfigInspectCommand(dockerCli), newConfigRemoveCommand(dockerCli), ) diff --git a/cli/command/config/events.go b/cli/command/config/events.go new file mode 100644 index 000000000000..c25f1dfab00e --- /dev/null +++ b/cli/command/config/events.go @@ -0,0 +1,13 @@ +package config + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newConfigEventsCommand creates a new cobra.Command for `docker secret events` +func newConfigEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.ConfigEventType) +} diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go index dcf8116ef1d0..696b6793f949 100644 --- a/cli/command/container/cmd.go +++ b/cli/command/container/cmd.go @@ -20,6 +20,7 @@ func NewContainerCommand(dockerCli command.Cli) *cobra.Command { NewCopyCommand(dockerCli), NewCreateCommand(dockerCli), NewDiffCommand(dockerCli), + newEventsCommand(dockerCli), NewExecCommand(dockerCli), NewExportCommand(dockerCli), NewKillCommand(dockerCli), diff --git a/cli/command/container/events.go b/cli/command/container/events.go new file mode 100644 index 000000000000..7ca5030c0ab9 --- /dev/null +++ b/cli/command/container/events.go @@ -0,0 +1,13 @@ +package container + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker container events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.ContainerEventType) +} diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go index c035da17f05c..8f5a88a0d091 100644 --- a/cli/command/image/cmd.go +++ b/cli/command/image/cmd.go @@ -16,6 +16,7 @@ func NewImageCommand(dockerCli command.Cli) *cobra.Command { } cmd.AddCommand( NewBuildCommand(dockerCli), + newEventsCommand(dockerCli), NewHistoryCommand(dockerCli), NewImportCommand(dockerCli), NewLoadCommand(dockerCli), diff --git a/cli/command/image/events.go b/cli/command/image/events.go new file mode 100644 index 000000000000..86871e7f83e1 --- /dev/null +++ b/cli/command/image/events.go @@ -0,0 +1,13 @@ +package image + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker image events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.ImageEventType) +} diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go index 3db3088ebe95..f58164d580de 100644 --- a/cli/command/network/cmd.go +++ b/cli/command/network/cmd.go @@ -19,6 +19,7 @@ func NewNetworkCommand(dockerCli command.Cli) *cobra.Command { newConnectCommand(dockerCli), newCreateCommand(dockerCli), newDisconnectCommand(dockerCli), + newEventsCommand(dockerCli), newInspectCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), diff --git a/cli/command/network/events.go b/cli/command/network/events.go new file mode 100644 index 000000000000..78a34ac87ef3 --- /dev/null +++ b/cli/command/network/events.go @@ -0,0 +1,13 @@ +package network + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker network events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.NetworkEventType) +} diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go index 0e519ae4405c..17962eb31eb7 100644 --- a/cli/command/node/cmd.go +++ b/cli/command/node/cmd.go @@ -25,6 +25,7 @@ func NewNodeCommand(dockerCli command.Cli) *cobra.Command { } cmd.AddCommand( newDemoteCommand(dockerCli), + newEventsCommand(dockerCli), newInspectCommand(dockerCli), newListCommand(dockerCli), newPromoteCommand(dockerCli), diff --git a/cli/command/node/events.go b/cli/command/node/events.go new file mode 100644 index 000000000000..d56496717a6f --- /dev/null +++ b/cli/command/node/events.go @@ -0,0 +1,13 @@ +package node + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker node events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.NodeEventType) +} diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index 2e79ab1d49b5..98643701e357 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -19,6 +19,7 @@ func NewPluginCommand(dockerCli command.Cli) *cobra.Command { cmd.AddCommand( newDisableCommand(dockerCli), newEnableCommand(dockerCli), + newEventsCommand(dockerCli), newInspectCommand(dockerCli), newInstallCommand(dockerCli), newListCommand(dockerCli), diff --git a/cli/command/plugin/events.go b/cli/command/plugin/events.go new file mode 100644 index 000000000000..cddaa5b96096 --- /dev/null +++ b/cli/command/plugin/events.go @@ -0,0 +1,13 @@ +package plugin + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker plugin events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.PluginEventType) +} diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go index ec0643b05440..000b2549e6f8 100644 --- a/cli/command/secret/cmd.go +++ b/cli/command/secret/cmd.go @@ -23,6 +23,7 @@ func NewSecretCommand(dockerCli command.Cli) *cobra.Command { cmd.AddCommand( newSecretListCommand(dockerCli), newSecretCreateCommand(dockerCli), + newSecretEventsCommand(dockerCli), newSecretInspectCommand(dockerCli), newSecretRemoveCommand(dockerCli), ) diff --git a/cli/command/secret/events.go b/cli/command/secret/events.go new file mode 100644 index 000000000000..d8caa8c80d18 --- /dev/null +++ b/cli/command/secret/events.go @@ -0,0 +1,13 @@ +package secret + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newSecretEventsCommand creates a new cobra.Command for `docker secret events` +func newSecretEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.SecretEventType) +} diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index 2c0b8fca9d15..d25d4ba4448c 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -22,6 +22,7 @@ func NewServiceCommand(dockerCli command.Cli) *cobra.Command { } cmd.AddCommand( newCreateCommand(dockerCli), + newEventsCommand(dockerCli), newInspectCommand(dockerCli), newPsCommand(dockerCli), newListCommand(dockerCli), diff --git a/cli/command/service/events.go b/cli/command/service/events.go new file mode 100644 index 000000000000..550f453acf29 --- /dev/null +++ b/cli/command/service/events.go @@ -0,0 +1,13 @@ +package service + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker service events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.ServiceEventType) +} diff --git a/cli/command/system/events.go b/cli/command/system/events.go index 7a632d8479a4..bf1e060cec8f 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -1,143 +1,29 @@ package system import ( - "context" - "fmt" - "io" - "sort" - "strings" - "text/template" - "time" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/events" "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/spf13/cobra" ) -type eventsOptions struct { - since string - until string - filter opts.FilterOpt - format string -} - // NewEventsCommand creates a new cobra.Command for `docker events` func NewEventsCommand(dockerCli command.Cli) *cobra.Command { - options := eventsOptions{filter: opts.NewFilterOpt()} + options := events.Options{Filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "events [OPTIONS]", Short: "Get real time events from the server", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(dockerCli, &options) + return events.Run(dockerCli, &options) }, ValidArgsFunction: completion.NoComplete, } flags := cmd.Flags() - 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") - + events.InstallFlags(flags, &options) return cmd } - -func runEvents(dockerCli command.Cli, options *eventsOptions) error { - tmpl, err := makeTemplate(options.format) - if err != nil { - return cli.StatusError{ - StatusCode: 64, - Status: "Error parsing format: " + err.Error()} - } - eventOptions := 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: - if err := handleEvent(out, event, tmpl); err != nil { - return err - } - case err := <-errs: - if err == io.EOF { - return nil - } - return err - } - } -} - -func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - if tmpl == nil { - return prettyPrintEvent(out, event) - } - - return formatEvent(out, event, tmpl) -} - -func makeTemplate(format string) (*template.Template, error) { - if format == "" { - return nil, nil - } - 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{}) -} - -// rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds -// zeros to maintain a fixed number of characters -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 { - if event.TimeNano != 0 { - fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed)) - } else if event.Time != 0 { - fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(rfc3339NanoFixed)) - } - - fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) - - if len(event.Actor.Attributes) > 0 { - var attrs []string - var keys []string - for k := range event.Actor.Attributes { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - v := event.Actor.Attributes[k] - attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) - } - fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) - } - fmt.Fprint(out, "\n") - return nil -} - -func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { - defer out.Write([]byte{'\n'}) - return tmpl.Execute(out, event) -} diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go index b2a552ae35e7..c4a61d4791c7 100644 --- a/cli/command/volume/cmd.go +++ b/cli/command/volume/cmd.go @@ -17,6 +17,7 @@ func NewVolumeCommand(dockerCli command.Cli) *cobra.Command { } cmd.AddCommand( newCreateCommand(dockerCli), + newEventsCommand(dockerCli), newInspectCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), diff --git a/cli/command/volume/events.go b/cli/command/volume/events.go new file mode 100644 index 000000000000..08c798b519a2 --- /dev/null +++ b/cli/command/volume/events.go @@ -0,0 +1,13 @@ +package volume + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/events" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/spf13/cobra" +) + +// newEventsCommand creates a new cobra.Command for `docker volume events` +func newEventsCommand(dockerCli command.Cli) *cobra.Command { + return events.NewObjectEventsCommand(dockerCli, eventtypes.VolumeEventType) +} diff --git a/cli/events/events.go b/cli/events/events.go new file mode 100644 index 000000000000..22c9aaf16bbf --- /dev/null +++ b/cli/events/events.go @@ -0,0 +1,164 @@ +package events + +import ( + "context" + "fmt" + "io" + "sort" + "strings" + "text/template" + "time" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "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/spf13/cobra" + "github.com/spf13/pflag" +) + +// Options specifies common options to use for system events. +type Options struct { + Type string + Since string + Until string + Filter opts.FilterOpt + Format string +} + +// NewObjectEventsCommand creates a new cobra.Command for the given object-type. +func NewObjectEventsCommand(dockerCli command.Cli, objectType string) *cobra.Command { + options := Options{ + Type: objectType, + Filter: opts.NewFilterOpt(), + } + + cmd := &cobra.Command{ + Use: "events [OPTIONS]", + Short: "Get real time " + options.Type + " events from the server", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return Run(dockerCli, &options) + }, + } + + flags := cmd.Flags() + InstallFlags(flags, &options) + return cmd +} + +// InstallFlags installs common flags for system events. +func InstallFlags(flags *pflag.FlagSet, options *Options) { + 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") +} + +// Run runs the events command +func Run(dockerCli command.Cli, options *Options) error { + if options.Type != "" { + if options.Filter.Value().Contains("type") { + return cli.StatusError{ + StatusCode: 64, + Status: "The --filter type= option is not supported when viewing " + options.Type + " events.", + } + } + options.Filter.Value().Add("type", options.Type) + } + + tmpl, err := makeTemplate(options.Format) + if err != nil { + return cli.StatusError{ + StatusCode: 64, + Status: "Error parsing format: " + err.Error(), + } + } + + eventOptions := 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: + if err := handleEvent(out, event, tmpl); err != nil { + return err + } + case err := <-errs: + if err == io.EOF { + return nil + } + return err + } + } +} + +func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { + if tmpl == nil { + return prettyPrintEvent(out, event) + } + + return formatEvent(out, event, tmpl) +} + +func makeTemplate(format string) (*template.Template, error) { + if format == "" { + return nil, nil + } + 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{}) +} + +// rfc3339NanoFixed is similar to time.RFC3339Nano, except it pads nanoseconds +// zeros to maintain a fixed number of characters +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 { + if event.TimeNano != 0 { + fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(rfc3339NanoFixed)) + } else if event.Time != 0 { + fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(rfc3339NanoFixed)) + } + + fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) + + if len(event.Actor.Attributes) > 0 { + var attrs []string + var keys []string + for k := range event.Actor.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := event.Actor.Attributes[k] + attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) + } + fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) + } + fmt.Fprint(out, "\n") + return nil +} + +func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { + defer out.Write([]byte{'\n'}) + return tmpl.Execute(out, event) +}