cli/command/container: runStats(): minor cleanup and fixes

- memoize the API-client in a local variable.
- use struct-literals in some places.
- rename some variables for clarity and to prevent colliding with imports.
- make use of the event-constants (events.ContainerEventType).
- fix some grammar
- fix some minor linting warnings

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2023-12-20 11:23:05 +01:00
parent 1f97a34ac2
commit 2389768fb7
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
1 changed files with 45 additions and 47 deletions

View File

@ -29,29 +29,29 @@ type statsOptions struct {
containers []string containers []string
} }
// NewStatsCommand creates a new cobra.Command for `docker stats` // NewStatsCommand creates a new [cobra.Command] for "docker stats".
func NewStatsCommand(dockerCli command.Cli) *cobra.Command { func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
var opts statsOptions var options statsOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "stats [OPTIONS] [CONTAINER...]", Use: "stats [OPTIONS] [CONTAINER...]",
Short: "Display a live stream of container(s) resource usage statistics", Short: "Display a live stream of container(s) resource usage statistics",
Args: cli.RequiresMinArgs(0), Args: cli.RequiresMinArgs(0),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.containers = args options.containers = args
return runStats(cmd.Context(), dockerCli, &opts) return runStats(cmd.Context(), dockerCLI, &options)
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"aliases": "docker container stats, docker stats", "aliases": "docker container stats, docker stats",
}, },
ValidArgsFunction: completion.ContainerNames(dockerCli, false), ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") flags.BoolVarP(&options.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") flags.BoolVar(&options.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output")
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
return cmd return cmd
} }
@ -59,22 +59,21 @@ func NewStatsCommand(dockerCli command.Cli) *cobra.Command {
// This shows real-time information on CPU usage, memory usage, and network I/O. // This shows real-time information on CPU usage, memory usage, and network I/O.
// //
//nolint:gocyclo //nolint:gocyclo
func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) error { func runStats(ctx context.Context, dockerCLI command.Cli, options *statsOptions) error {
showAll := len(opts.containers) == 0 showAll := len(options.containers) == 0
closeChan := make(chan error) closeChan := make(chan error)
apiClient := dockerCLI.Client()
// monitorContainerEvents watches for container creation and removal (only // monitorContainerEvents watches for container creation and removal (only
// used when calling `docker stats` without arguments). // used when calling `docker stats` without arguments).
monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) { monitorContainerEvents := func(started chan<- struct{}, c chan events.Message, stopped <-chan struct{}) {
f := filters.NewArgs() f := filters.NewArgs()
f.Add("type", "container") f.Add("type", string(events.ContainerEventType))
options := types.EventsOptions{ eventChan, errChan := apiClient.Events(ctx, types.EventsOptions{
Filters: f, Filters: f,
} })
eventq, errq := dockerCli.Client().Events(ctx, options) // Whether we successfully subscribed to eventChan or not, we can now
// Whether we successfully subscribed to eventq or not, we can now
// unblock the main goroutine. // unblock the main goroutine.
close(started) close(started)
defer close(c) defer close(c)
@ -83,9 +82,9 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
select { select {
case <-stopped: case <-stopped:
return return
case event := <-eventq: case event := <-eventChan:
c <- event c <- event
case err := <-errq: case err := <-errChan:
closeChan <- err closeChan <- err
return return
} }
@ -94,7 +93,7 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
// Get the daemonOSType if not set already // Get the daemonOSType if not set already
if daemonOSType == "" { if daemonOSType == "" {
sv, err := dockerCli.Client().ServerVersion(ctx) sv, err := apiClient.ServerVersion(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -108,10 +107,9 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
// getContainerList simulates creation event for all previously existing // getContainerList simulates creation event for all previously existing
// containers (only used when calling `docker stats` without arguments). // containers (only used when calling `docker stats` without arguments).
getContainerList := func() { getContainerList := func() {
options := container.ListOptions{ cs, err := apiClient.ContainerList(ctx, container.ListOptions{
All: opts.all, All: options.all,
} })
cs, err := dockerCli.Client().ContainerList(ctx, options)
if err != nil { if err != nil {
closeChan <- err closeChan <- err
} }
@ -119,24 +117,24 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
s := NewStats(ctr.ID[:12]) s := NewStats(ctr.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, apiClient, !options.noStream, waitFirst)
} }
} }
} }
if showAll { if showAll {
// If no names were specified, start a long running goroutine which // If no names were specified, start a long-running goroutine which
// monitors container events. We make sure we're subscribed before // monitors container events. We make sure we're subscribed before
// retrieving the list of running containers to avoid a race where we // retrieving the list of running containers to avoid a race where we
// would "miss" a creation. // would "miss" a creation.
started := make(chan struct{}) started := make(chan struct{})
eh := command.InitEventHandler() eh := command.InitEventHandler()
eh.Handle(events.ActionCreate, func(e events.Message) { eh.Handle(events.ActionCreate, func(e events.Message) {
if opts.all { if options.all {
s := NewStats(e.Actor.ID[:12]) s := NewStats(e.Actor.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, apiClient, !options.noStream, waitFirst)
} }
} }
}) })
@ -145,12 +143,12 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
s := NewStats(e.Actor.ID[:12]) s := NewStats(e.Actor.ID[:12])
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, apiClient, !options.noStream, waitFirst)
} }
}) })
eh.Handle(events.ActionDie, func(e events.Message) { eh.Handle(events.ActionDie, func(e events.Message) {
if !opts.all { if !options.all {
cStats.remove(e.Actor.ID[:12]) cStats.remove(e.Actor.ID[:12])
} }
}) })
@ -171,11 +169,11 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
} else { } else {
// Artificially send creation events for the containers we were asked to // Artificially send creation events for the containers we were asked to
// monitor (same code path than we use when monitoring all containers). // monitor (same code path than we use when monitoring all containers).
for _, name := range opts.containers { for _, name := range options.containers {
s := NewStats(name) s := NewStats(name)
if cStats.add(s) { if cStats.add(s) {
waitFirst.Add(1) waitFirst.Add(1)
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) go collect(ctx, s, apiClient, !options.noStream, waitFirst)
} }
} }
@ -198,22 +196,22 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
} }
} }
format := opts.format format := options.format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCli.ConfigFile().StatsFormat) > 0 { if len(dockerCLI.ConfigFile().StatsFormat) > 0 {
format = dockerCli.ConfigFile().StatsFormat format = dockerCLI.ConfigFile().StatsFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }
} }
statsCtx := formatter.Context{ statsCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCLI.Out(),
Format: NewStatsFormat(format, daemonOSType), Format: NewStatsFormat(format, daemonOSType),
} }
cleanScreen := func() { cleanScreen := func() {
if !opts.noStream { if !options.noStream {
fmt.Fprint(dockerCli.Out(), "\033[2J") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
fmt.Fprint(dockerCli.Out(), "\033[H") _, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
} }
} }
@ -222,28 +220,28 @@ func runStats(ctx context.Context, dockerCli command.Cli, opts *statsOptions) er
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
cleanScreen() cleanScreen()
ccstats := []StatsEntry{} var ccStats []StatsEntry
cStats.mu.RLock() cStats.mu.RLock()
for _, c := range cStats.cs { for _, c := range cStats.cs {
ccstats = append(ccstats, c.GetStatistics()) ccStats = append(ccStats, c.GetStatistics())
} }
cStats.mu.RUnlock() cStats.mu.RUnlock()
if err = statsFormatWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil { if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.noTrunc); err != nil {
break break
} }
if len(cStats.cs) == 0 && !showAll { if len(cStats.cs) == 0 && !showAll {
break break
} }
if opts.noStream { if options.noStream {
break break
} }
select { select {
case err, ok := <-closeChan: case err, ok := <-closeChan:
if ok { if ok {
if err != nil { if err != nil {
// this is suppressing "unexpected EOF" in the cli when the // Suppress "unexpected EOF" errors in the CLI so that
// daemon restarts so it shutdowns cleanly // it shuts down cleanly when the daemon restarts.
if err == io.ErrUnexpectedEOF { if errors.Is(err, io.ErrUnexpectedEOF) {
return nil return nil
} }
return err return err