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

     1  package formatter
     2  
     3  import (
     4  	"strconv"
     5  	"time"
     6  
     7  	"github.com/distribution/reference"
     8  	"github.com/docker/docker/api/types/image"
     9  	"github.com/docker/docker/pkg/stringid"
    10  	units "github.com/docker/go-units"
    11  )
    12  
    13  const (
    14  	defaultImageTableFormat           = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
    15  	defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{if .CreatedSince }}{{.CreatedSince}}{{else}}N/A{{end}}\t{{.Size}}"
    16  
    17  	imageIDHeader    = "IMAGE ID"
    18  	repositoryHeader = "REPOSITORY"
    19  	tagHeader        = "TAG"
    20  	digestHeader     = "DIGEST"
    21  )
    22  
    23  // ImageContext contains image specific information required by the formatter, encapsulate a Context struct.
    24  type ImageContext struct {
    25  	Context
    26  	Digest bool
    27  }
    28  
    29  func isDangling(img image.Summary) bool {
    30  	if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
    31  		return true
    32  	}
    33  	return len(img.RepoTags) == 1 && img.RepoTags[0] == "<none>:<none>" && len(img.RepoDigests) == 1 && img.RepoDigests[0] == "<none>@<none>"
    34  }
    35  
    36  // NewImageFormat returns a format for rendering an ImageContext
    37  func NewImageFormat(source string, quiet bool, digest bool) Format {
    38  	switch source {
    39  	case TableFormatKey:
    40  		switch {
    41  		case quiet:
    42  			return DefaultQuietFormat
    43  		case digest:
    44  			return defaultImageTableFormatWithDigest
    45  		default:
    46  			return defaultImageTableFormat
    47  		}
    48  	case RawFormatKey:
    49  		switch {
    50  		case quiet:
    51  			return `image_id: {{.ID}}`
    52  		case digest:
    53  			return `repository: {{ .Repository }}
    54  tag: {{.Tag}}
    55  digest: {{.Digest}}
    56  image_id: {{.ID}}
    57  created_at: {{.CreatedAt}}
    58  virtual_size: {{.Size}}
    59  `
    60  		default:
    61  			return `repository: {{ .Repository }}
    62  tag: {{.Tag}}
    63  image_id: {{.ID}}
    64  created_at: {{.CreatedAt}}
    65  virtual_size: {{.Size}}
    66  `
    67  		}
    68  	}
    69  
    70  	format := Format(source)
    71  	if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
    72  		format += "\t{{.Digest}}"
    73  	}
    74  	return format
    75  }
    76  
    77  // ImageWrite writes the formatter images using the ImageContext
    78  func ImageWrite(ctx ImageContext, images []image.Summary) error {
    79  	render := func(format func(subContext SubContext) error) error {
    80  		return imageFormat(ctx, images, format)
    81  	}
    82  	return ctx.Write(newImageContext(), render)
    83  }
    84  
    85  // needDigest determines whether the image digest should be ignored or not when writing image context
    86  func needDigest(ctx ImageContext) bool {
    87  	return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
    88  }
    89  
    90  func imageFormat(ctx ImageContext, images []image.Summary, format func(subContext SubContext) error) error {
    91  	for _, img := range images {
    92  		formatted := []*imageContext{}
    93  		if isDangling(img) {
    94  			formatted = append(formatted, &imageContext{
    95  				trunc:  ctx.Trunc,
    96  				i:      img,
    97  				repo:   "<none>",
    98  				tag:    "<none>",
    99  				digest: "<none>",
   100  			})
   101  		} else {
   102  			formatted = imageFormatTaggedAndDigest(ctx, img)
   103  		}
   104  		for _, imageCtx := range formatted {
   105  			if err := format(imageCtx); err != nil {
   106  				return err
   107  			}
   108  		}
   109  	}
   110  	return nil
   111  }
   112  
   113  func imageFormatTaggedAndDigest(ctx ImageContext, img image.Summary) []*imageContext {
   114  	repoTags := map[string][]string{}
   115  	repoDigests := map[string][]string{}
   116  	images := []*imageContext{}
   117  
   118  	for _, refString := range img.RepoTags {
   119  		ref, err := reference.ParseNormalizedNamed(refString)
   120  		if err != nil {
   121  			continue
   122  		}
   123  		if nt, ok := ref.(reference.NamedTagged); ok {
   124  			familiarRef := reference.FamiliarName(ref)
   125  			repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
   126  		}
   127  	}
   128  	for _, refString := range img.RepoDigests {
   129  		ref, err := reference.ParseNormalizedNamed(refString)
   130  		if err != nil {
   131  			continue
   132  		}
   133  		if c, ok := ref.(reference.Canonical); ok {
   134  			familiarRef := reference.FamiliarName(ref)
   135  			repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
   136  		}
   137  	}
   138  
   139  	addImage := func(repo, tag, digest string) {
   140  		images = append(images, &imageContext{
   141  			trunc:  ctx.Trunc,
   142  			i:      img,
   143  			repo:   repo,
   144  			tag:    tag,
   145  			digest: digest,
   146  		})
   147  	}
   148  
   149  	for repo, tags := range repoTags {
   150  		digests := repoDigests[repo]
   151  
   152  		// Do not display digests as their own row
   153  		delete(repoDigests, repo)
   154  
   155  		if !needDigest(ctx) {
   156  			// Ignore digest references, just show tag once
   157  			digests = nil
   158  		}
   159  
   160  		for _, tag := range tags {
   161  			if len(digests) == 0 {
   162  				addImage(repo, tag, "<none>")
   163  				continue
   164  			}
   165  			// Display the digests for each tag
   166  			for _, dgst := range digests {
   167  				addImage(repo, tag, dgst)
   168  			}
   169  		}
   170  	}
   171  
   172  	// Show rows for remaining digest only references
   173  	for repo, digests := range repoDigests {
   174  		// If digests are displayed, show row per digest
   175  		if ctx.Digest {
   176  			for _, dgst := range digests {
   177  				addImage(repo, "<none>", dgst)
   178  			}
   179  		} else {
   180  			addImage(repo, "<none>", "")
   181  		}
   182  	}
   183  	return images
   184  }
   185  
   186  type imageContext struct {
   187  	HeaderContext
   188  	trunc  bool
   189  	i      image.Summary
   190  	repo   string
   191  	tag    string
   192  	digest string
   193  }
   194  
   195  func newImageContext() *imageContext {
   196  	imageCtx := imageContext{}
   197  	imageCtx.Header = SubHeaderContext{
   198  		"ID":           imageIDHeader,
   199  		"Repository":   repositoryHeader,
   200  		"Tag":          tagHeader,
   201  		"Digest":       digestHeader,
   202  		"CreatedSince": CreatedSinceHeader,
   203  		"CreatedAt":    CreatedAtHeader,
   204  		"Size":         SizeHeader,
   205  		"Containers":   containersHeader,
   206  		"VirtualSize":  SizeHeader, // Deprecated: VirtualSize is deprecated, and equivalent to Size.
   207  		"SharedSize":   sharedSizeHeader,
   208  		"UniqueSize":   uniqueSizeHeader,
   209  	}
   210  	return &imageCtx
   211  }
   212  
   213  func (c *imageContext) MarshalJSON() ([]byte, error) {
   214  	return MarshalJSON(c)
   215  }
   216  
   217  func (c *imageContext) ID() string {
   218  	if c.trunc {
   219  		return stringid.TruncateID(c.i.ID)
   220  	}
   221  	return c.i.ID
   222  }
   223  
   224  func (c *imageContext) Repository() string {
   225  	return c.repo
   226  }
   227  
   228  func (c *imageContext) Tag() string {
   229  	return c.tag
   230  }
   231  
   232  func (c *imageContext) Digest() string {
   233  	return c.digest
   234  }
   235  
   236  func (c *imageContext) CreatedSince() string {
   237  	createdAt := time.Unix(c.i.Created, 0)
   238  
   239  	if createdAt.IsZero() {
   240  		return ""
   241  	}
   242  
   243  	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
   244  }
   245  
   246  func (c *imageContext) CreatedAt() string {
   247  	return time.Unix(c.i.Created, 0).String()
   248  }
   249  
   250  func (c *imageContext) Size() string {
   251  	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
   252  }
   253  
   254  func (c *imageContext) Containers() string {
   255  	if c.i.Containers == -1 {
   256  		return "N/A"
   257  	}
   258  	return strconv.FormatInt(c.i.Containers, 10)
   259  }
   260  
   261  // VirtualSize shows the virtual size of the image and all of its parent
   262  // images. Starting with docker 1.10, images are self-contained, and
   263  // the VirtualSize is identical to Size.
   264  //
   265  // Deprecated: VirtualSize is deprecated, and equivalent to [imageContext.Size].
   266  func (c *imageContext) VirtualSize() string {
   267  	return units.HumanSize(float64(c.i.Size))
   268  }
   269  
   270  func (c *imageContext) SharedSize() string {
   271  	if c.i.SharedSize == -1 {
   272  		return "N/A"
   273  	}
   274  	return units.HumanSize(float64(c.i.SharedSize))
   275  }
   276  
   277  func (c *imageContext) UniqueSize() string {
   278  	if c.i.Size == -1 || c.i.SharedSize == -1 {
   279  		return "N/A"
   280  	}
   281  	return units.HumanSize(float64(c.i.Size - c.i.SharedSize))
   282  }