180 lines
5.3 KiB
Go
180 lines
5.3 KiB
Go
//go:build !remote
|
|
|
|
package libimage
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/containers/storage"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// ImageDiskUsage reports the total size of an image. That is the size
|
|
type ImageDiskUsage struct {
|
|
// Number of containers using the image.
|
|
Containers int
|
|
// ID of the image.
|
|
ID string
|
|
// Repository of the image.
|
|
Repository string
|
|
// Tag of the image.
|
|
Tag string
|
|
// Created time stamp.
|
|
Created time.Time
|
|
// The amount of space that an image shares with another one (i.e. their common data).
|
|
SharedSize int64
|
|
// The the amount of space that is only used by a given image.
|
|
UniqueSize int64
|
|
// Sum of shared an unique size.
|
|
Size int64
|
|
}
|
|
|
|
// DiskUsage calculates the disk usage for each image in the local containers
|
|
// storage. Note that a single image may yield multiple usage reports, one for
|
|
// each repository tag.
|
|
func (r *Runtime) DiskUsage(ctx context.Context) ([]ImageDiskUsage, int64, error) {
|
|
images, layers, err := r.getImagesAndLayers()
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
|
|
var totalSize int64
|
|
layerMap := make(map[string]*storage.Layer)
|
|
for _, layer := range layers {
|
|
layerMap[layer.ID] = &layer
|
|
if layer.UncompressedSize == -1 {
|
|
// size is unknown, we must manually diff the layer size which
|
|
// can be quite slow as it might have to walk all files
|
|
size, err := r.store.DiffSize("", layer.ID)
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
// cache the size now
|
|
layer.UncompressedSize = size
|
|
}
|
|
// count the total layer size here so we know we only count each layer once
|
|
totalSize += layer.UncompressedSize
|
|
}
|
|
|
|
// First walk all images to count how often each layer is used.
|
|
// This is done so we know if the size for an image is shared between
|
|
// images that use the same layer or unique.
|
|
layerCount := make(map[string]int)
|
|
for _, image := range images {
|
|
walkImageLayers(image, layerMap, func(layer *storage.Layer) {
|
|
// Increment the count for each layer visit
|
|
layerCount[layer.ID] += 1
|
|
})
|
|
}
|
|
|
|
// Now that we actually have all the info walk again to add the sizes.
|
|
var allUsages []ImageDiskUsage
|
|
for _, image := range images {
|
|
usages, err := diskUsageForImage(ctx, image, layerMap, layerCount, &totalSize)
|
|
if err != nil {
|
|
return nil, -1, err
|
|
}
|
|
allUsages = append(allUsages, usages...)
|
|
}
|
|
return allUsages, totalSize, err
|
|
}
|
|
|
|
// diskUsageForImage returns the disk-usage baseistics for the specified image.
|
|
func diskUsageForImage(ctx context.Context, image *Image, layerMap map[string]*storage.Layer, layerCount map[string]int, totalSize *int64) ([]ImageDiskUsage, error) {
|
|
if err := image.isCorrupted(ctx, ""); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
base := ImageDiskUsage{
|
|
ID: image.ID(),
|
|
Created: image.Created(),
|
|
Repository: "<none>",
|
|
Tag: "<none>",
|
|
}
|
|
|
|
walkImageLayers(image, layerMap, func(layer *storage.Layer) {
|
|
// If the layer used by more than one image it shares its size
|
|
if layerCount[layer.ID] > 1 {
|
|
base.SharedSize += layer.UncompressedSize
|
|
} else {
|
|
base.UniqueSize += layer.UncompressedSize
|
|
}
|
|
})
|
|
|
|
// Count the image specific big data as well.
|
|
// store.BigDataSize() is not used intentionally, it is slower (has to take
|
|
// locks) and can fail.
|
|
// BigDataSizes is always correctly populated on new stores since c/storage
|
|
// commit a7d7fe8c9a (2016). It should be safe to assume that no such old
|
|
// store+image exist now so we don't bother. Worst case we report a few
|
|
// bytes to little.
|
|
for _, size := range image.storageImage.BigDataSizes {
|
|
base.UniqueSize += size
|
|
*totalSize += size
|
|
}
|
|
|
|
base.Size = base.SharedSize + base.UniqueSize
|
|
|
|
// Number of containers using the image.
|
|
containers, err := image.Containers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base.Containers = len(containers)
|
|
|
|
repoTags, err := image.NamedRepoTags()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(repoTags) == 0 {
|
|
return []ImageDiskUsage{base}, nil
|
|
}
|
|
|
|
pairs, err := ToNameTagPairs(repoTags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results := make([]ImageDiskUsage, len(pairs))
|
|
for i, pair := range pairs {
|
|
res := base
|
|
res.Repository = pair.Name
|
|
res.Tag = pair.Tag
|
|
results[i] = res
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// walkImageLayers walks all layers in an image and calls the given function for each layer.
|
|
func walkImageLayers(image *Image, layerMap map[string]*storage.Layer, f func(layer *storage.Layer)) {
|
|
visited := make(map[string]struct{})
|
|
// Layers are walked recursively until it has no parent which means we reached the end.
|
|
// We must account for the fact that an image might have several top layers when id mappings are used.
|
|
layers := append([]string{image.storageImage.TopLayer}, image.storageImage.MappedTopLayers...)
|
|
for _, layerID := range layers {
|
|
for layerID != "" {
|
|
layer := layerMap[layerID]
|
|
if layer == nil {
|
|
logrus.Errorf("Local Storage is corrupt, layer %q missing from the storage", layerID)
|
|
break
|
|
}
|
|
if _, ok := visited[layerID]; ok {
|
|
// We have seen this layer before. Break here to
|
|
// a) Do not count the same layer twice that was shared between
|
|
// the TopLayer and MappedTopLayers layer chain.
|
|
// b) Prevent infinite loops, should not happen per c/storage
|
|
// design but it is good to be safer.
|
|
break
|
|
}
|
|
visited[layerID] = struct{}{}
|
|
|
|
f(layer)
|
|
// Set the layer for the next iteration, parent is empty if we reach the end.
|
|
layerID = layer.Parent
|
|
}
|
|
}
|
|
}
|