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

     1  package formatter
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  	"text/template"
     9  
    10  	"github.com/docker/distribution/reference"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/volume"
    13  	units "github.com/docker/go-units"
    14  )
    15  
    16  const (
    17  	defaultDiskUsageImageTableFormat      = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
    18  	defaultDiskUsageContainerTableFormat  = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
    19  	defaultDiskUsageVolumeTableFormat     = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
    20  	defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
    21  	defaultDiskUsageTableFormat           = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
    22  
    23  	typeHeader        = "TYPE"
    24  	totalHeader       = "TOTAL"
    25  	activeHeader      = "ACTIVE"
    26  	reclaimableHeader = "RECLAIMABLE"
    27  	containersHeader  = "CONTAINERS"
    28  	sharedSizeHeader  = "SHARED SIZE"
    29  	uniqueSizeHeader  = "UNIQUE SIZE"
    30  )
    31  
    32  // DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
    33  type DiskUsageContext struct {
    34  	Context
    35  	Verbose     bool
    36  	LayersSize  int64
    37  	Images      []*types.ImageSummary
    38  	Containers  []*types.Container
    39  	Volumes     []*volume.Volume
    40  	BuildCache  []*types.BuildCache
    41  	BuilderSize int64
    42  }
    43  
    44  func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
    45  	ctx.buffer = bytes.NewBufferString("")
    46  	ctx.header = ""
    47  	ctx.Format = Format(format)
    48  	ctx.preFormat()
    49  
    50  	return ctx.parseFormat()
    51  }
    52  
    53  // NewDiskUsageFormat returns a format for rendering an DiskUsageContext
    54  func NewDiskUsageFormat(source string, verbose bool) Format {
    55  	switch {
    56  	case verbose && source == RawFormatKey:
    57  		format := `{{range .Images}}type: Image
    58  ` + NewImageFormat(source, false, true) + `
    59  {{end -}}
    60  {{range .Containers}}type: Container
    61  ` + NewContainerFormat(source, false, true) + `
    62  {{end -}}
    63  {{range .Volumes}}type: Volume
    64  ` + NewVolumeFormat(source, false) + `
    65  {{end -}}
    66  {{range .BuildCache}}type: Build Cache
    67  ` + NewBuildCacheFormat(source, false) + `
    68  {{end -}}`
    69  		return format
    70  	case !verbose && source == TableFormatKey:
    71  		return Format(defaultDiskUsageTableFormat)
    72  	case !verbose && source == RawFormatKey:
    73  		format := `type: {{.Type}}
    74  total: {{.TotalCount}}
    75  active: {{.Active}}
    76  size: {{.Size}}
    77  reclaimable: {{.Reclaimable}}
    78  `
    79  		return Format(format)
    80  	default:
    81  		return Format(source)
    82  	}
    83  }
    84  
    85  func (ctx *DiskUsageContext) Write() (err error) {
    86  	if ctx.Verbose {
    87  		return ctx.verboseWrite()
    88  	}
    89  	ctx.buffer = bytes.NewBufferString("")
    90  	ctx.preFormat()
    91  
    92  	tmpl, err := ctx.parseFormat()
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
    98  		totalSize: ctx.LayersSize,
    99  		images:    ctx.Images,
   100  	})
   101  	if err != nil {
   102  		return err
   103  	}
   104  	err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
   105  		containers: ctx.Containers,
   106  	})
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
   112  		volumes: ctx.Volumes,
   113  	})
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{
   119  		builderSize: ctx.BuilderSize,
   120  		buildCache:  ctx.BuildCache,
   121  	})
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
   127  	diskUsageContainersCtx.Header = SubHeaderContext{
   128  		"Type":        typeHeader,
   129  		"TotalCount":  totalHeader,
   130  		"Active":      activeHeader,
   131  		"Size":        SizeHeader,
   132  		"Reclaimable": reclaimableHeader,
   133  	}
   134  	ctx.postFormat(tmpl, &diskUsageContainersCtx)
   135  
   136  	return err
   137  }
   138  
   139  type diskUsageContext struct {
   140  	Images     []*imageContext
   141  	Containers []*ContainerContext
   142  	Volumes    []*volumeContext
   143  	BuildCache []*buildCacheContext
   144  }
   145  
   146  func (ctx *DiskUsageContext) verboseWrite() error {
   147  	duc := &diskUsageContext{
   148  		Images:     make([]*imageContext, 0, len(ctx.Images)),
   149  		Containers: make([]*ContainerContext, 0, len(ctx.Containers)),
   150  		Volumes:    make([]*volumeContext, 0, len(ctx.Volumes)),
   151  		BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
   152  	}
   153  	trunc := ctx.Format.IsTable()
   154  
   155  	// First images
   156  	for _, i := range ctx.Images {
   157  		repo := "<none>"
   158  		tag := "<none>"
   159  		if len(i.RepoTags) > 0 && !isDangling(*i) {
   160  			// Only show the first tag
   161  			ref, err := reference.ParseNormalizedNamed(i.RepoTags[0])
   162  			if err != nil {
   163  				continue
   164  			}
   165  			if nt, ok := ref.(reference.NamedTagged); ok {
   166  				repo = reference.FamiliarName(ref)
   167  				tag = nt.Tag()
   168  			}
   169  		}
   170  
   171  		duc.Images = append(duc.Images, &imageContext{
   172  			repo:  repo,
   173  			tag:   tag,
   174  			trunc: trunc,
   175  			i:     *i,
   176  		})
   177  	}
   178  
   179  	// Now containers
   180  	for _, c := range ctx.Containers {
   181  		// Don't display the virtual size
   182  		c.SizeRootFs = 0
   183  		duc.Containers = append(duc.Containers, &ContainerContext{trunc: trunc, c: *c})
   184  	}
   185  
   186  	// And volumes
   187  	for _, v := range ctx.Volumes {
   188  		duc.Volumes = append(duc.Volumes, &volumeContext{v: *v})
   189  	}
   190  
   191  	// And build cache
   192  	buildCacheSort(ctx.BuildCache)
   193  	for _, v := range ctx.BuildCache {
   194  		duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc})
   195  	}
   196  
   197  	if ctx.Format == TableFormatKey {
   198  		return ctx.verboseWriteTable(duc)
   199  	}
   200  
   201  	ctx.preFormat()
   202  	tmpl, err := ctx.parseFormat()
   203  	if err != nil {
   204  		return err
   205  	}
   206  	return tmpl.Execute(ctx.Output, duc)
   207  }
   208  
   209  func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
   210  	tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	ctx.Output.Write([]byte("Images space usage:\n\n"))
   215  	for _, img := range duc.Images {
   216  		if err := ctx.contextFormat(tmpl, img); err != nil {
   217  			return err
   218  		}
   219  	}
   220  	ctx.postFormat(tmpl, newImageContext())
   221  
   222  	tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
   227  	for _, c := range duc.Containers {
   228  		if err := ctx.contextFormat(tmpl, c); err != nil {
   229  			return err
   230  		}
   231  	}
   232  	ctx.postFormat(tmpl, NewContainerContext())
   233  
   234  	tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
   239  	for _, v := range duc.Volumes {
   240  		if err := ctx.contextFormat(tmpl, v); err != nil {
   241  			return err
   242  		}
   243  	}
   244  	ctx.postFormat(tmpl, newVolumeContext())
   245  
   246  	tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
   251  	for _, v := range duc.BuildCache {
   252  		if err := ctx.contextFormat(tmpl, v); err != nil {
   253  			return err
   254  		}
   255  	}
   256  	ctx.postFormat(tmpl, newBuildCacheContext())
   257  
   258  	return nil
   259  }
   260  
   261  type diskUsageImagesContext struct {
   262  	HeaderContext
   263  	totalSize int64
   264  	images    []*types.ImageSummary
   265  }
   266  
   267  func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) {
   268  	return MarshalJSON(c)
   269  }
   270  
   271  func (c *diskUsageImagesContext) Type() string {
   272  	return "Images"
   273  }
   274  
   275  func (c *diskUsageImagesContext) TotalCount() string {
   276  	return strconv.Itoa(len(c.images))
   277  }
   278  
   279  func (c *diskUsageImagesContext) Active() string {
   280  	used := 0
   281  	for _, i := range c.images {
   282  		if i.Containers > 0 {
   283  			used++
   284  		}
   285  	}
   286  
   287  	return strconv.Itoa(used)
   288  }
   289  
   290  func (c *diskUsageImagesContext) Size() string {
   291  	return units.HumanSize(float64(c.totalSize))
   292  }
   293  
   294  func (c *diskUsageImagesContext) Reclaimable() string {
   295  	var used int64
   296  
   297  	for _, i := range c.images {
   298  		if i.Containers != 0 {
   299  			if i.VirtualSize == -1 || i.SharedSize == -1 {
   300  				continue
   301  			}
   302  			used += i.VirtualSize - i.SharedSize
   303  		}
   304  	}
   305  
   306  	reclaimable := c.totalSize - used
   307  	if c.totalSize > 0 {
   308  		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
   309  	}
   310  	return units.HumanSize(float64(reclaimable))
   311  }
   312  
   313  type diskUsageContainersContext struct {
   314  	HeaderContext
   315  	containers []*types.Container
   316  }
   317  
   318  func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) {
   319  	return MarshalJSON(c)
   320  }
   321  
   322  func (c *diskUsageContainersContext) Type() string {
   323  	return "Containers"
   324  }
   325  
   326  func (c *diskUsageContainersContext) TotalCount() string {
   327  	return strconv.Itoa(len(c.containers))
   328  }
   329  
   330  func (c *diskUsageContainersContext) isActive(container types.Container) bool {
   331  	return strings.Contains(container.State, "running") ||
   332  		strings.Contains(container.State, "paused") ||
   333  		strings.Contains(container.State, "restarting")
   334  }
   335  
   336  func (c *diskUsageContainersContext) Active() string {
   337  	used := 0
   338  	for _, container := range c.containers {
   339  		if c.isActive(*container) {
   340  			used++
   341  		}
   342  	}
   343  
   344  	return strconv.Itoa(used)
   345  }
   346  
   347  func (c *diskUsageContainersContext) Size() string {
   348  	var size int64
   349  
   350  	for _, container := range c.containers {
   351  		size += container.SizeRw
   352  	}
   353  
   354  	return units.HumanSize(float64(size))
   355  }
   356  
   357  func (c *diskUsageContainersContext) Reclaimable() string {
   358  	var reclaimable int64
   359  	var totalSize int64
   360  
   361  	for _, container := range c.containers {
   362  		if !c.isActive(*container) {
   363  			reclaimable += container.SizeRw
   364  		}
   365  		totalSize += container.SizeRw
   366  	}
   367  
   368  	if totalSize > 0 {
   369  		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
   370  	}
   371  
   372  	return units.HumanSize(float64(reclaimable))
   373  }
   374  
   375  type diskUsageVolumesContext struct {
   376  	HeaderContext
   377  	volumes []*volume.Volume
   378  }
   379  
   380  func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) {
   381  	return MarshalJSON(c)
   382  }
   383  
   384  func (c *diskUsageVolumesContext) Type() string {
   385  	return "Local Volumes"
   386  }
   387  
   388  func (c *diskUsageVolumesContext) TotalCount() string {
   389  	return strconv.Itoa(len(c.volumes))
   390  }
   391  
   392  func (c *diskUsageVolumesContext) Active() string {
   393  	used := 0
   394  	for _, v := range c.volumes {
   395  		if v.UsageData.RefCount > 0 {
   396  			used++
   397  		}
   398  	}
   399  
   400  	return strconv.Itoa(used)
   401  }
   402  
   403  func (c *diskUsageVolumesContext) Size() string {
   404  	var size int64
   405  
   406  	for _, v := range c.volumes {
   407  		if v.UsageData.Size != -1 {
   408  			size += v.UsageData.Size
   409  		}
   410  	}
   411  
   412  	return units.HumanSize(float64(size))
   413  }
   414  
   415  func (c *diskUsageVolumesContext) Reclaimable() string {
   416  	var reclaimable int64
   417  	var totalSize int64
   418  
   419  	for _, v := range c.volumes {
   420  		if v.UsageData.Size != -1 {
   421  			if v.UsageData.RefCount == 0 {
   422  				reclaimable += v.UsageData.Size
   423  			}
   424  			totalSize += v.UsageData.Size
   425  		}
   426  	}
   427  
   428  	if totalSize > 0 {
   429  		return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
   430  	}
   431  
   432  	return units.HumanSize(float64(reclaimable))
   433  }
   434  
   435  type diskUsageBuilderContext struct {
   436  	HeaderContext
   437  	builderSize int64
   438  	buildCache  []*types.BuildCache
   439  }
   440  
   441  func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
   442  	return MarshalJSON(c)
   443  }
   444  
   445  func (c *diskUsageBuilderContext) Type() string {
   446  	return "Build Cache"
   447  }
   448  
   449  func (c *diskUsageBuilderContext) TotalCount() string {
   450  	return strconv.Itoa(len(c.buildCache))
   451  }
   452  
   453  func (c *diskUsageBuilderContext) Active() string {
   454  	numActive := 0
   455  	for _, bc := range c.buildCache {
   456  		if bc.InUse {
   457  			numActive++
   458  		}
   459  	}
   460  	return strconv.Itoa(numActive)
   461  }
   462  
   463  func (c *diskUsageBuilderContext) Size() string {
   464  	return units.HumanSize(float64(c.builderSize))
   465  }
   466  
   467  func (c *diskUsageBuilderContext) Reclaimable() string {
   468  	var inUseBytes int64
   469  	for _, bc := range c.buildCache {
   470  		if bc.InUse && !bc.Shared {
   471  			inUseBytes += bc.Size
   472  		}
   473  	}
   474  
   475  	return units.HumanSize(float64(c.builderSize - inUseBytes))
   476  }