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 }