github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli/command/formatter/container.go (about)

     1  package formatter
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/docker/distribution/reference"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/pkg/stringid"
    13  	"github.com/docker/go-units"
    14  )
    15  
    16  const (
    17  	defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
    18  
    19  	namesHeader      = "NAMES"
    20  	commandHeader    = "COMMAND"
    21  	runningForHeader = "CREATED"
    22  	mountsHeader     = "MOUNTS"
    23  	localVolumes     = "LOCAL VOLUMES"
    24  	networksHeader   = "NETWORKS"
    25  )
    26  
    27  // NewContainerFormat returns a Format for rendering using a Context
    28  func NewContainerFormat(source string, quiet bool, size bool) Format {
    29  	switch source {
    30  	case TableFormatKey:
    31  		if quiet {
    32  			return DefaultQuietFormat
    33  		}
    34  		format := defaultContainerTableFormat
    35  		if size {
    36  			format += `\t{{.Size}}`
    37  		}
    38  		return Format(format)
    39  	case RawFormatKey:
    40  		if quiet {
    41  			return `container_id: {{.ID}}`
    42  		}
    43  		format := `container_id: {{.ID}}
    44  image: {{.Image}}
    45  command: {{.Command}}
    46  created_at: {{.CreatedAt}}
    47  state: {{- pad .State 1 0}}
    48  status: {{- pad .Status 1 0}}
    49  names: {{.Names}}
    50  labels: {{- pad .Labels 1 0}}
    51  ports: {{- pad .Ports 1 0}}
    52  `
    53  		if size {
    54  			format += `size: {{.Size}}\n`
    55  		}
    56  		return Format(format)
    57  	}
    58  	return Format(source)
    59  }
    60  
    61  // ContainerWrite renders the context for a list of containers
    62  func ContainerWrite(ctx Context, containers []types.Container) error {
    63  	render := func(format func(subContext SubContext) error) error {
    64  		for _, container := range containers {
    65  			err := format(&ContainerContext{trunc: ctx.Trunc, c: container})
    66  			if err != nil {
    67  				return err
    68  			}
    69  		}
    70  		return nil
    71  	}
    72  	return ctx.Write(NewContainerContext(), render)
    73  }
    74  
    75  // ContainerContext is a struct used for rendering a list of containers in a Go template.
    76  type ContainerContext struct {
    77  	HeaderContext
    78  	trunc bool
    79  	c     types.Container
    80  
    81  	// FieldsUsed is used in the pre-processing step to detect which fields are
    82  	// used in the template. It's currently only used to detect use of the .Size
    83  	// field which (if used) automatically sets the '--size' option when making
    84  	// the API call.
    85  	FieldsUsed map[string]interface{}
    86  }
    87  
    88  // NewContainerContext creates a new context for rendering containers
    89  func NewContainerContext() *ContainerContext {
    90  	containerCtx := ContainerContext{}
    91  	containerCtx.Header = SubHeaderContext{
    92  		"ID":           ContainerIDHeader,
    93  		"Names":        namesHeader,
    94  		"Image":        ImageHeader,
    95  		"Command":      commandHeader,
    96  		"CreatedAt":    CreatedAtHeader,
    97  		"RunningFor":   runningForHeader,
    98  		"Ports":        PortsHeader,
    99  		"State":        StateHeader,
   100  		"Status":       StatusHeader,
   101  		"Size":         SizeHeader,
   102  		"Labels":       LabelsHeader,
   103  		"Mounts":       mountsHeader,
   104  		"LocalVolumes": localVolumes,
   105  		"Networks":     networksHeader,
   106  	}
   107  	return &containerCtx
   108  }
   109  
   110  // MarshalJSON makes ContainerContext implement json.Marshaler
   111  func (c *ContainerContext) MarshalJSON() ([]byte, error) {
   112  	return MarshalJSON(c)
   113  }
   114  
   115  // ID returns the container's ID as a string. Depending on the `--no-trunc`
   116  // option being set, the full or truncated ID is returned.
   117  func (c *ContainerContext) ID() string {
   118  	if c.trunc {
   119  		return stringid.TruncateID(c.c.ID)
   120  	}
   121  	return c.c.ID
   122  }
   123  
   124  // Names returns a comma-separated string of the container's names, with their
   125  // slash (/) prefix stripped. Additional names for the container (related to the
   126  // legacy `--link` feature) are omitted.
   127  func (c *ContainerContext) Names() string {
   128  	names := stripNamePrefix(c.c.Names)
   129  	if c.trunc {
   130  		for _, name := range names {
   131  			if len(strings.Split(name, "/")) == 1 {
   132  				names = []string{name}
   133  				break
   134  			}
   135  		}
   136  	}
   137  	return strings.Join(names, ",")
   138  }
   139  
   140  // Image returns the container's image reference. If the trunc option is set,
   141  // the image's registry digest can be included.
   142  func (c *ContainerContext) Image() string {
   143  	if c.c.Image == "" {
   144  		return "<no image>"
   145  	}
   146  	if c.trunc {
   147  		if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
   148  			return trunc
   149  		}
   150  		// truncate digest if no-trunc option was not selected
   151  		ref, err := reference.ParseNormalizedNamed(c.c.Image)
   152  		if err == nil {
   153  			if nt, ok := ref.(reference.NamedTagged); ok {
   154  				// case for when a tag is provided
   155  				if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil {
   156  					return reference.FamiliarString(namedTagged)
   157  				}
   158  			} else {
   159  				// case for when a tag is not provided
   160  				named := reference.TrimNamed(ref)
   161  				return reference.FamiliarString(named)
   162  			}
   163  		}
   164  	}
   165  
   166  	return c.c.Image
   167  }
   168  
   169  // Command returns's the container's command. If the trunc option is set, the
   170  // returned command is truncated (ellipsized).
   171  func (c *ContainerContext) Command() string {
   172  	command := c.c.Command
   173  	if c.trunc {
   174  		command = Ellipsis(command, 20)
   175  	}
   176  	return strconv.Quote(command)
   177  }
   178  
   179  // CreatedAt returns the "Created" date/time of the container as a unix timestamp.
   180  func (c *ContainerContext) CreatedAt() string {
   181  	return time.Unix(c.c.Created, 0).String()
   182  }
   183  
   184  // RunningFor returns a human-readable representation of the duration for which
   185  // the container has been running.
   186  //
   187  // Note that this duration is calculated on the client, and as such is influenced
   188  // by clock skew between the client and the daemon.
   189  func (c *ContainerContext) RunningFor() string {
   190  	createdAt := time.Unix(c.c.Created, 0)
   191  	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
   192  }
   193  
   194  // Ports returns a comma-separated string representing open ports of the container
   195  // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
   196  // it's used by command 'docker ps'
   197  // Both published and exposed ports are included.
   198  func (c *ContainerContext) Ports() string {
   199  	return DisplayablePorts(c.c.Ports)
   200  }
   201  
   202  // State returns the container's current state (e.g. "running" or "paused")
   203  func (c *ContainerContext) State() string {
   204  	return c.c.State
   205  }
   206  
   207  // Status returns the container's status in a human readable form (for example,
   208  // "Up 24 hours" or "Exited (0) 8 days ago")
   209  func (c *ContainerContext) Status() string {
   210  	return c.c.Status
   211  }
   212  
   213  // Size returns the container's size and virtual size (e.g. "2B (virtual 21.5MB)")
   214  func (c *ContainerContext) Size() string {
   215  	if c.FieldsUsed == nil {
   216  		c.FieldsUsed = map[string]interface{}{}
   217  	}
   218  	c.FieldsUsed["Size"] = struct{}{}
   219  	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
   220  	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
   221  
   222  	sf := srw
   223  	if c.c.SizeRootFs > 0 {
   224  		sf = fmt.Sprintf("%s (virtual %s)", srw, sv)
   225  	}
   226  	return sf
   227  }
   228  
   229  // Labels returns a comma-separated string of labels present on the container.
   230  func (c *ContainerContext) Labels() string {
   231  	if c.c.Labels == nil {
   232  		return ""
   233  	}
   234  
   235  	var joinLabels []string
   236  	for k, v := range c.c.Labels {
   237  		joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v))
   238  	}
   239  	return strings.Join(joinLabels, ",")
   240  }
   241  
   242  // Label returns the value of the label with the given name or an empty string
   243  // if the given label does not exist.
   244  func (c *ContainerContext) Label(name string) string {
   245  	if c.c.Labels == nil {
   246  		return ""
   247  	}
   248  	return c.c.Labels[name]
   249  }
   250  
   251  // Mounts returns a comma-separated string of mount names present on the container.
   252  // If the trunc option is set, names can be truncated (ellipsized).
   253  func (c *ContainerContext) Mounts() string {
   254  	var name string
   255  	var mounts []string
   256  	for _, m := range c.c.Mounts {
   257  		if m.Name == "" {
   258  			name = m.Source
   259  		} else {
   260  			name = m.Name
   261  		}
   262  		if c.trunc {
   263  			name = Ellipsis(name, 15)
   264  		}
   265  		mounts = append(mounts, name)
   266  	}
   267  	return strings.Join(mounts, ",")
   268  }
   269  
   270  // LocalVolumes returns the number of volumes using the "local" volume driver.
   271  func (c *ContainerContext) LocalVolumes() string {
   272  	count := 0
   273  	for _, m := range c.c.Mounts {
   274  		if m.Driver == "local" {
   275  			count++
   276  		}
   277  	}
   278  
   279  	return fmt.Sprintf("%d", count)
   280  }
   281  
   282  // Networks returns a comma-separated string of networks that the container is
   283  // attached to.
   284  func (c *ContainerContext) Networks() string {
   285  	if c.c.NetworkSettings == nil {
   286  		return ""
   287  	}
   288  
   289  	networks := []string{}
   290  	for k := range c.c.NetworkSettings.Networks {
   291  		networks = append(networks, k)
   292  	}
   293  
   294  	return strings.Join(networks, ",")
   295  }
   296  
   297  // DisplayablePorts returns formatted string representing open ports of container
   298  // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
   299  // it's used by command 'docker ps'
   300  func DisplayablePorts(ports []types.Port) string {
   301  	type portGroup struct {
   302  		first uint16
   303  		last  uint16
   304  	}
   305  	groupMap := make(map[string]*portGroup)
   306  	var result []string
   307  	var hostMappings []string
   308  	var groupMapKeys []string
   309  	sort.Slice(ports, func(i, j int) bool {
   310  		return comparePorts(ports[i], ports[j])
   311  	})
   312  
   313  	for _, port := range ports {
   314  		current := port.PrivatePort
   315  		portKey := port.Type
   316  		if port.IP != "" {
   317  			if port.PublicPort != current {
   318  				hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
   319  				continue
   320  			}
   321  			portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
   322  		}
   323  		group := groupMap[portKey]
   324  
   325  		if group == nil {
   326  			groupMap[portKey] = &portGroup{first: current, last: current}
   327  			// record order that groupMap keys are created
   328  			groupMapKeys = append(groupMapKeys, portKey)
   329  			continue
   330  		}
   331  		if current == (group.last + 1) {
   332  			group.last = current
   333  			continue
   334  		}
   335  
   336  		result = append(result, formGroup(portKey, group.first, group.last))
   337  		groupMap[portKey] = &portGroup{first: current, last: current}
   338  	}
   339  	for _, portKey := range groupMapKeys {
   340  		g := groupMap[portKey]
   341  		result = append(result, formGroup(portKey, g.first, g.last))
   342  	}
   343  	result = append(result, hostMappings...)
   344  	return strings.Join(result, ", ")
   345  }
   346  
   347  func formGroup(key string, start, last uint16) string {
   348  	parts := strings.Split(key, "/")
   349  	groupType := parts[0]
   350  	var ip string
   351  	if len(parts) > 1 {
   352  		ip = parts[0]
   353  		groupType = parts[1]
   354  	}
   355  	group := strconv.Itoa(int(start))
   356  	if start != last {
   357  		group = fmt.Sprintf("%s-%d", group, last)
   358  	}
   359  	if ip != "" {
   360  		group = fmt.Sprintf("%s:%s->%s", ip, group, group)
   361  	}
   362  	return fmt.Sprintf("%s/%s", group, groupType)
   363  }
   364  
   365  func comparePorts(i, j types.Port) bool {
   366  	if i.PrivatePort != j.PrivatePort {
   367  		return i.PrivatePort < j.PrivatePort
   368  	}
   369  
   370  	if i.IP != j.IP {
   371  		return i.IP < j.IP
   372  	}
   373  
   374  	if i.PublicPort != j.PublicPort {
   375  		return i.PublicPort < j.PublicPort
   376  	}
   377  
   378  	return i.Type < j.Type
   379  }