github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/command/formatter/image.go (about)

     1  package formatter
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/docker/distribution/reference"
     8  	"github.com/docker/docker/api/types"
     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(image types.ImageSummary) bool {
    30  	if len(image.RepoTags) == 0 && len(image.RepoDigests) == 0 {
    31  		return true
    32  	}
    33  	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.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 []types.ImageSummary) 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 []types.ImageSummary, format func(subContext SubContext) error) error {
    91  	for _, image := range images {
    92  		formatted := []*imageContext{}
    93  		if isDangling(image) {
    94  			formatted = append(formatted, &imageContext{
    95  				trunc:  ctx.Trunc,
    96  				i:      image,
    97  				repo:   "<none>",
    98  				tag:    "<none>",
    99  				digest: "<none>",
   100  			})
   101  		} else {
   102  			formatted = imageFormatTaggedAndDigest(ctx, image)
   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, image types.ImageSummary) []*imageContext {
   114  	repoTags := map[string][]string{}
   115  	repoDigests := map[string][]string{}
   116  	images := []*imageContext{}
   117  
   118  	for _, refString := range image.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 image.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  		image := &imageContext{
   141  			trunc:  ctx.Trunc,
   142  			i:      image,
   143  			repo:   repo,
   144  			tag:    tag,
   145  			digest: digest,
   146  		}
   147  		images = append(images, image)
   148  	}
   149  
   150  	for repo, tags := range repoTags {
   151  		digests := repoDigests[repo]
   152  
   153  		// Do not display digests as their own row
   154  		delete(repoDigests, repo)
   155  
   156  		if !needDigest(ctx) {
   157  			// Ignore digest references, just show tag once
   158  			digests = nil
   159  		}
   160  
   161  		for _, tag := range tags {
   162  			if len(digests) == 0 {
   163  				addImage(repo, tag, "<none>")
   164  				continue
   165  			}
   166  			// Display the digests for each tag
   167  			for _, dgst := range digests {
   168  				addImage(repo, tag, dgst)
   169  			}
   170  
   171  		}
   172  	}
   173  
   174  	// Show rows for remaining digest only references
   175  	for repo, digests := range repoDigests {
   176  		// If digests are displayed, show row per digest
   177  		if ctx.Digest {
   178  			for _, dgst := range digests {
   179  				addImage(repo, "<none>", dgst)
   180  			}
   181  		} else {
   182  			addImage(repo, "<none>", "")
   183  		}
   184  	}
   185  	return images
   186  }
   187  
   188  type imageContext struct {
   189  	HeaderContext
   190  	trunc  bool
   191  	i      types.ImageSummary
   192  	repo   string
   193  	tag    string
   194  	digest string
   195  }
   196  
   197  func newImageContext() *imageContext {
   198  	imageCtx := imageContext{}
   199  	imageCtx.Header = SubHeaderContext{
   200  		"ID":           imageIDHeader,
   201  		"Repository":   repositoryHeader,
   202  		"Tag":          tagHeader,
   203  		"Digest":       digestHeader,
   204  		"CreatedSince": CreatedSinceHeader,
   205  		"CreatedAt":    CreatedAtHeader,
   206  		"Size":         SizeHeader,
   207  		"Containers":   containersHeader,
   208  		"VirtualSize":  SizeHeader,
   209  		"SharedSize":   sharedSizeHeader,
   210  		"UniqueSize":   uniqueSizeHeader,
   211  	}
   212  	return &imageCtx
   213  }
   214  
   215  func (c *imageContext) MarshalJSON() ([]byte, error) {
   216  	return MarshalJSON(c)
   217  }
   218  
   219  func (c *imageContext) ID() string {
   220  	if c.trunc {
   221  		return stringid.TruncateID(c.i.ID)
   222  	}
   223  	return c.i.ID
   224  }
   225  
   226  func (c *imageContext) Repository() string {
   227  	return c.repo
   228  }
   229  
   230  func (c *imageContext) Tag() string {
   231  	return c.tag
   232  }
   233  
   234  func (c *imageContext) Digest() string {
   235  	return c.digest
   236  }
   237  
   238  func (c *imageContext) CreatedSince() string {
   239  	createdAt := time.Unix(c.i.Created, 0)
   240  
   241  	if createdAt.IsZero() {
   242  		return ""
   243  	}
   244  
   245  	return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
   246  }
   247  
   248  func (c *imageContext) CreatedAt() string {
   249  	return time.Unix(c.i.Created, 0).String()
   250  }
   251  
   252  func (c *imageContext) Size() string {
   253  	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
   254  }
   255  
   256  func (c *imageContext) Containers() string {
   257  	if c.i.Containers == -1 {
   258  		return "N/A"
   259  	}
   260  	return fmt.Sprintf("%d", c.i.Containers)
   261  }
   262  
   263  func (c *imageContext) VirtualSize() string {
   264  	return units.HumanSize(float64(c.i.VirtualSize))
   265  }
   266  
   267  func (c *imageContext) SharedSize() string {
   268  	if c.i.SharedSize == -1 {
   269  		return "N/A"
   270  	}
   271  	return units.HumanSize(float64(c.i.SharedSize))
   272  }
   273  
   274  func (c *imageContext) UniqueSize() string {
   275  	if c.i.VirtualSize == -1 || c.i.SharedSize == -1 {
   276  		return "N/A"
   277  	}
   278  	return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize))
   279  }