docker / cli

The Docker CLI
Apache License 2.0
4.94k stars 1.93k forks source link

The --format flag table directive does not work with docker inspect #1103

Open mikebski opened 6 years ago

mikebski commented 6 years ago

docker service inspect --format 'table {{.Spec.Name}}\t{{range .Endpoint.VirtualIPs}}{{.Addr}} {{end}}' $(docker service ls -q) This is not printing a table, it's printing:

container1\t10.0.2.3 107.2.30.4

container2\t10.0.2.5 107.2.30.8

...etc...

Steps to reproduce the issue:

  1. Deploy a container as a service: docker service create ngnix will work
  2. Run command: docker service inspect --format 'table {{.Spec.Name}}\t{{range .Endpoint.VirtualIPs}}{{.Addr}} {{end}}' $(docker service ls -q)

Describe the results you received:

table sharp_goldstine\t

*Note service has no IP address, that's fine here

Describe the results you expected:

Table output similar to docker stats with 2 columns

Output of docker version:

Client:
 Version:      18.03.1-ce
 API version:  1.37
 Go version:   go1.9.5
 Git commit:   9ee9f40
 Built:        Thu Apr 26 07:13:02 2018
 OS/Arch:      darwin/amd64
 Experimental: false
 Orchestrator: swarm

Server:
 Engine:
  Version:      18.03.1-ce
  API version:  1.37 (minimum version 1.12)
  Go version:   go1.9.5
  Git commit:   9ee9f40
  Built:        Thu Apr 26 07:22:38 2018
  OS/Arch:      linux/amd64
  Experimental: true

Output of docker info:

Containers: 16
 Running: 9
 Paused: 0
 Stopped: 7
Images: 49
Server Version: 18.03.1-ce
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host ipvlan macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: active
 NodeID: daldy4p8ynukd4mlf9p1wv1vx
 Is Manager: true
 ClusterID: hccuyor98zeifze8mexdc4uw0
 Managers: 1
 Nodes: 1
 Orchestration:
  Task History Retention Limit: 5
 Raft:
  Snapshot Interval: 10000
  Number of Old Snapshots to Retain: 0
  Heartbeat Tick: 1
  Election Tick: 3
 Dispatcher:
  Heartbeat Period: 5 seconds
 CA Configuration:
  Expiry Duration: 3 months
  Force Rotate: 0
 Autolock Managers: false
 Root Rotation In Progress: false
 Node Address: 192.168.65.2
 Manager Addresses:
  192.168.65.2:2377
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 773c489c9c1b21a6d78b5c538cd395416ec50f88
runc version: 4fc53a81fb7c994640722ac585fa9ca548971871
init version: 949e6fa
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.87-linuxkit-aufs
Operating System: Docker for Mac
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 1.952GiB
Name: linuxkit-025000000001
ID: TXEI:TAOU:SCOP:BGAB:6KTA:3TZ3:46QJ:GORK:45NX:3K7J:MHFC:YXNY
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 106
 Goroutines: 243
 System Time: 2018-06-01T11:40:48.2385353Z
 EventsListeners: 7
HTTP Proxy: docker.for.mac.http.internal:3128
HTTPS Proxy: docker.for.mac.http.internal:3129
Registry: https://index.docker.io/v1/
Labels:
Experimental: true
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

Additional environment details (AWS, VirtualBox, physical, etc.):

Mac laptop

cwgem commented 6 years ago

TLDR; docker inspect deals with JSON output and thus JSON output templates, versus the formatting strings you'd expect from other commands (such as docker stats

I think the confusion stems from docker inspect not having the same --format functionality as other traditional command such as docker stats which you mentioned. When formatting command output commands provide a format context and a writer. Stats for example:

func NewStatsFormat(source, osType string) Format {
    if source == TableFormatKey {
        if osType == winOSType {
            return Format(winDefaultStatsTableFormat)
        }
        return Format(defaultStatsTableFormat)
    }
    return Format(source)
}

https://github.com/docker/cli/blob/master/cli/command/formatter/stats.go#L102

So based on various constraints it either returns a default format string (if you just pass in table as the format string for example) or just passes the format string provided as-is. Then you have the writer which renders the template given the format:

func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string, trunc bool) error {
    render := func(format func(subContext subContext) error) error {
        for _, cstats := range containerStats {
            containerStatsCtx := &containerStatsContext{
                s:     cstats,
                os:    osType,
                trunc: trunc,
            }
            if err := format(containerStatsCtx); err != nil {
                return err
            }
        }
        return nil
    }
    memUsage := memUseHeader
    if osType == winOSType {
        memUsage = winMemUseHeader
    }
    containerStatsCtx := containerStatsContext{}
    containerStatsCtx.header = map[string]string{
        "Container": containerHeader,
        "Name":      nameHeader,
        "ID":        containerIDHeader,
        "CPUPerc":   cpuPercHeader,
        "MemUsage":  memUsage,
        "MemPerc":   memPercHeader,
        "NetIO":     netIOHeader,
        "BlockIO":   blockIOHeader,
        "PIDs":      pidsHeader,
    }
    containerStatsCtx.os = osType
    return ctx.Write(&containerStatsCtx, render)
}

https://github.com/docker/cli/blob/master/cli/command/formatter/stats.go#L118

Now with the inspect command it takes a different path:

// ServiceInspectWrite renders the context for a list of services
func ServiceInspectWrite(ctx Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error {
    if ctx.Format != serviceInspectPrettyTemplate {
        return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
    }

https://github.com/docker/cli/blob/master/cli/command/formatter/service.go#L170

Unless you have pretty format enabled it does an inspect.Inspect call:

// NewTemplateInspectorFromString creates a new TemplateInspector from a string
// which is compiled into a template.
func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) {
    if tmplStr == "" {
        return NewIndentedInspector(out), nil
    }

    tmpl, err := templates.Parse(tmplStr)
    if err != nil {
        return nil, errors.Errorf("Template parsing error: %s", err)
    }
    return NewTemplateInspector(out, tmpl), nil
}

// -------- SNIP ------------

func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error {
    inspector, err := NewTemplateInspectorFromString(out, tmplStr)

This in turn uses a different string format including different base functions:

var basicFunctions = template.FuncMap{
    "json": func(v interface{}) string {
        buf := &bytes.Buffer{}
        enc := json.NewEncoder(buf)
        enc.SetEscapeHTML(false)
        enc.Encode(v)
        // Remove the trailing new line added by the encoder
        return strings.TrimSpace(buf.String())
    },
    "split":    strings.Split,
    "join":     strings.Join,
    "title":    strings.Title,
    "lower":    strings.ToLower,
    "upper":    strings.ToUpper,
    "pad":      padWithSpace,
    "truncate": truncateWithLength,
}

https://github.com/docker/cli/blob/master/templates/templates.go#L12

Which is essentially why table isn't working as expected and coming back as raw. That said it might be good to have a --template-format instead to make it a bit less confusing.