github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/formatter/container.go (about)

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