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