diff --git a/libimage/history.go b/libimage/history.go index 845f4088e..096280fc6 100644 --- a/libimage/history.go +++ b/libimage/history.go @@ -25,7 +25,7 @@ func (i *Image) History(ctx context.Context) ([]ImageHistory, error) { return nil, err } - layerTree, err := i.runtime.newFreshLayerTree() + layerTree, err := i.runtime.newFreshLayerTree(ctx) if err != nil { return nil, err } diff --git a/libimage/image.go b/libimage/image.go index d680537b1..ea7c0e6fb 100644 --- a/libimage/image.go +++ b/libimage/image.go @@ -191,13 +191,21 @@ func (i *Image) IsReadOnly() bool { } // IsDangling returns true if the image is dangling, that is an untagged image -// without children. +// without children and not used in a manifest list. func (i *Image) IsDangling(ctx context.Context) (bool, error) { - return i.isDangling(ctx, nil) + images, layers, err := i.runtime.getImagesAndLayers() + if err != nil { + return false, err + } + tree, err := i.runtime.newLayerTreeFromData(ctx, images, layers, true) + if err != nil { + return false, err + } + return i.isDangling(ctx, tree) } // isDangling returns true if the image is dangling, that is an untagged image -// without children. If tree is nil, it will created for this invocation only. +// without children and not used in a manifest list. If tree is nil, it will created for this invocation only. func (i *Image) isDangling(ctx context.Context, tree *layerTree) (bool, error) { if len(i.Names()) > 0 { return false, nil @@ -206,7 +214,8 @@ func (i *Image) isDangling(ctx context.Context, tree *layerTree) (bool, error) { if err != nil { return false, err } - return len(children) == 0, nil + _, usedInManfiestList := tree.manifestListDigests[i.Digest()] + return (len(children) == 0 && !usedInManfiestList), nil } // IsIntermediate returns true if the image is an intermediate image, that is @@ -258,7 +267,7 @@ func (i *Image) TopLayer() string { // Parent returns the parent image or nil if there is none func (i *Image) Parent(ctx context.Context) (*Image, error) { - tree, err := i.runtime.newFreshLayerTree() + tree, err := i.runtime.newFreshLayerTree(ctx) if err != nil { return nil, err } @@ -292,7 +301,7 @@ func (i *Image) Children(ctx context.Context) ([]*Image, error) { // created for this invocation only. func (i *Image) getChildren(ctx context.Context, all bool, tree *layerTree) ([]*Image, error) { if tree == nil { - t, err := i.runtime.newFreshLayerTree() + t, err := i.runtime.newFreshLayerTree(ctx) if err != nil { return nil, err } diff --git a/libimage/image_tree.go b/libimage/image_tree.go index 9628a319f..0c30b8a55 100644 --- a/libimage/image_tree.go +++ b/libimage/image_tree.go @@ -3,6 +3,7 @@ package libimage import ( + "context" "fmt" "strings" @@ -13,7 +14,7 @@ import ( // Tree generates a tree for the specified image and its layers. Use // `traverseChildren` to traverse the layers of all children. By default, only // layers of the image are printed. -func (i *Image) Tree(traverseChildren bool) (string, error) { +func (i *Image) Tree(ctx context.Context, traverseChildren bool) (string, error) { // NOTE: a string builder prevents us from copying to much data around // and compile the string when and where needed. sb := &strings.Builder{} @@ -37,7 +38,7 @@ func (i *Image) Tree(traverseChildren bool) (string, error) { fmt.Fprintf(sb, "No Image Layers") } - layerTree, err := i.runtime.newFreshLayerTree() + layerTree, err := i.runtime.newFreshLayerTree(ctx) if err != nil { return "", err } diff --git a/libimage/layer_tree.go b/libimage/layer_tree.go index 33741bd08..2c548933c 100644 --- a/libimage/layer_tree.go +++ b/libimage/layer_tree.go @@ -8,6 +8,7 @@ import ( "github.com/containers/storage" storageTypes "github.com/containers/storage/types" + digest "github.com/opencontainers/go-digest" ociv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" ) @@ -22,6 +23,10 @@ type layerTree struct { // emptyImages do not have any top-layer so we cannot create a // *layerNode for them. emptyImages []*Image + // manifestList keep track of images based on their digest. + // Library will use this map when checking if a image is dangling. + // If an image is used in a manifestList it is NOT dangling + manifestListDigests map[digest.Digest]struct{} } // node returns a layerNode for the specified layerID. @@ -90,20 +95,21 @@ func (l *layerNode) repoTags() ([]string, error) { } // newFreshLayerTree extracts a layerTree from consistent layers and images in the local storage. -func (r *Runtime) newFreshLayerTree() (*layerTree, error) { +func (r *Runtime) newFreshLayerTree(ctx context.Context) (*layerTree, error) { images, layers, err := r.getImagesAndLayers() if err != nil { return nil, err } - return r.newLayerTreeFromData(images, layers) + return r.newLayerTreeFromData(ctx, images, layers, false) } // newLayerTreeFromData extracts a layerTree from the given the layers and images. // The caller is responsible for (layers, images) being consistent. -func (r *Runtime) newLayerTreeFromData(images []*Image, layers []storage.Layer) (*layerTree, error) { +func (r *Runtime) newLayerTreeFromData(ctx context.Context, images []*Image, layers []storage.Layer, generateManifestDigestList bool) (*layerTree, error) { tree := layerTree{ - nodes: make(map[string]*layerNode), - ociCache: make(map[string]*ociv1.Image), + nodes: make(map[string]*layerNode), + ociCache: make(map[string]*ociv1.Image), + manifestListDigests: make(map[digest.Digest]struct{}), } // First build a tree purely based on layer information. @@ -124,6 +130,21 @@ func (r *Runtime) newLayerTreeFromData(images []*Image, layers []storage.Layer) topLayer := img.TopLayer() if topLayer == "" { tree.emptyImages = append(tree.emptyImages, img) + // When img is a manifest list, cache the lists of + // digests refereenced in manifest list. Digests can + // be used to check for dangling images. + if !generateManifestDigestList { + continue + } + if manifestList, _ := img.IsManifestList(ctx); manifestList { + mlist, err := img.ToManifestList() + if err != nil { + return nil, err + } + for _, digest := range mlist.list.Instances() { + tree.manifestListDigests[digest] = struct{}{} + } + } continue } node, exists := tree.nodes[topLayer] diff --git a/libimage/runtime.go b/libimage/runtime.go index f7c9b2bf9..1e01a5f0e 100644 --- a/libimage/runtime.go +++ b/libimage/runtime.go @@ -634,7 +634,7 @@ func (r *Runtime) ListImages(ctx context.Context, options *ListImagesOptions) ([ var tree *layerTree if needsLayerTree { - tree, err = r.newLayerTreeFromData(images, snapshot.Layers) + tree, err = r.newLayerTreeFromData(ctx, images, snapshot.Layers, true) if err != nil { return nil, err }