dotcloud/docker

View on GitHub
image/cache/cache.go

Summary

Maintainability
A
2 hrs
Test Coverage
package cache // import "github.com/docker/docker/image/cache"

import (
    "context"
    "fmt"
    "reflect"
    "strings"

    "github.com/containerd/log"
    containertypes "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/builder"
    "github.com/docker/docker/dockerversion"
    "github.com/docker/docker/image"
    "github.com/docker/docker/layer"
    ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    "github.com/pkg/errors"
)

type ImageCacheStore interface {
    Get(image.ID) (*image.Image, error)
    GetByRef(ctx context.Context, refOrId string) (*image.Image, error)
    SetParent(target, parent image.ID) error
    GetParent(target image.ID) (image.ID, error)
    Create(parent *image.Image, image image.Image, extraLayer layer.DiffID) (image.ID, error)
    IsBuiltLocally(id image.ID) (bool, error)
    Children(id image.ID) []image.ID
}

func New(ctx context.Context, store ImageCacheStore, cacheFrom []string) (builder.ImageCache, error) {
    local := &LocalImageCache{store: store}
    if len(cacheFrom) == 0 {
        return local, nil
    }

    cache := &ImageCache{
        store:           store,
        localImageCache: local,
    }

    for _, ref := range cacheFrom {
        img, err := store.GetByRef(ctx, ref)
        if err != nil {
            if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
                return nil, err
            }
            log.G(ctx).Warnf("Could not look up %s for cache resolution, skipping: %+v", ref, err)
            continue
        }
        cache.Populate(img)
    }

    return cache, nil
}

// LocalImageCache is cache based on parent chain.
type LocalImageCache struct {
    store ImageCacheStore
}

// GetCache returns the image id found in the cache
func (lic *LocalImageCache) GetCache(imgID string, config *containertypes.Config, platform ocispec.Platform) (string, error) {
    return getImageIDAndError(getLocalCachedImage(lic.store, image.ID(imgID), config, platform))
}

// ImageCache is cache based on history objects. Requires initial set of images.
type ImageCache struct {
    sources         []*image.Image
    store           ImageCacheStore
    localImageCache *LocalImageCache
}

// Populate adds an image to the cache (to be queried later)
func (ic *ImageCache) Populate(image *image.Image) {
    ic.sources = append(ic.sources, image)
}

// GetCache returns the image id found in the cache
func (ic *ImageCache) GetCache(parentID string, cfg *containertypes.Config, platform ocispec.Platform) (string, error) {
    imgID, err := ic.localImageCache.GetCache(parentID, cfg, platform)
    if err != nil {
        return "", err
    }
    if imgID != "" {
        for _, s := range ic.sources {
            if ic.isParent(s.ID(), image.ID(imgID)) {
                return imgID, nil
            }
        }
    }

    var parent *image.Image
    lenHistory := 0
    if parentID != "" {
        parent, err = ic.store.Get(image.ID(parentID))
        if err != nil {
            return "", errors.Wrapf(err, "unable to find image %v", parentID)
        }
        lenHistory = len(parent.History)
    }

    for _, target := range ic.sources {
        if !isValidParent(target, parent) || !isValidConfig(cfg, target.History[lenHistory]) {
            continue
        }

        if len(target.History)-1 == lenHistory { // last
            if parent != nil {
                if err := ic.store.SetParent(target.ID(), parent.ID()); err != nil {
                    return "", errors.Wrapf(err, "failed to set parent for %v to %v", target.ID(), parent.ID())
                }
            }
            return target.ID().String(), nil
        }

        imgID, err := ic.restoreCachedImage(parent, target, cfg)
        if err != nil {
            return "", errors.Wrapf(err, "failed to restore cached image from %q to %v", parentID, target.ID())
        }

        ic.sources = []*image.Image{target} // avoid jumping to different target, tuned for safety atm
        return imgID.String(), nil
    }

    return "", nil
}

func (ic *ImageCache) restoreCachedImage(parent, target *image.Image, cfg *containertypes.Config) (image.ID, error) {
    var history []image.History
    rootFS := image.NewRootFS()
    lenHistory := 0
    if parent != nil {
        history = parent.History
        rootFS = parent.RootFS
        lenHistory = len(parent.History)
    }
    history = append(history, target.History[lenHistory])
    layer := getLayerForHistoryIndex(target, lenHistory)
    if layer != "" {
        rootFS.Append(layer)
    }

    restoredImg := image.Image{
        V1Image: image.V1Image{
            DockerVersion: dockerversion.Version,
            Config:        cfg,
            Architecture:  target.Architecture,
            OS:            target.OS,
            Author:        target.Author,
            Created:       history[len(history)-1].Created,
        },
        RootFS:     rootFS,
        History:    history,
        OSFeatures: target.OSFeatures,
        OSVersion:  target.OSVersion,
    }

    imgID, err := ic.store.Create(parent, restoredImg, layer)
    if err != nil {
        return "", errors.Wrap(err, "failed to create cache image")
    }

    return imgID, nil
}

func (ic *ImageCache) isParent(imgID, parentID image.ID) bool {
    nextParent, err := ic.store.GetParent(imgID)
    if err != nil {
        return false
    }
    if nextParent == parentID {
        return true
    }
    return ic.isParent(nextParent, parentID)
}

func getLayerForHistoryIndex(image *image.Image, index int) layer.DiffID {
    layerIndex := 0
    for i, h := range image.History {
        if i == index {
            if h.EmptyLayer {
                return ""
            }
            break
        }
        if !h.EmptyLayer {
            layerIndex++
        }
    }
    return image.RootFS.DiffIDs[layerIndex] // validate?
}

func isValidConfig(cfg *containertypes.Config, h image.History) bool {
    // todo: make this format better than join that loses data
    return strings.Join(cfg.Cmd, " ") == h.CreatedBy
}

func isValidParent(img, parent *image.Image) bool {
    if len(img.History) == 0 {
        return false
    }
    if parent == nil || len(parent.History) == 0 && len(parent.RootFS.DiffIDs) == 0 {
        return true
    }
    if len(parent.History) >= len(img.History) {
        return false
    }
    if len(parent.RootFS.DiffIDs) > len(img.RootFS.DiffIDs) {
        return false
    }

    for i, h := range parent.History {
        if !reflect.DeepEqual(h, img.History[i]) {
            return false
        }
    }
    for i, d := range parent.RootFS.DiffIDs {
        if d != img.RootFS.DiffIDs[i] {
            return false
        }
    }
    return true
}

func getImageIDAndError(img *image.Image, err error) (string, error) {
    if img == nil || err != nil {
        return "", err
    }
    return img.ID().String(), nil
}

// getLocalCachedImage returns the most recent created image that is a child
// of the image with imgID, that had the same config when it was
// created. nil is returned if a child cannot be found. An error is
// returned if the parent image cannot be found.
func getLocalCachedImage(imageStore ImageCacheStore, parentID image.ID, config *containertypes.Config, platform ocispec.Platform) (*image.Image, error) {
    if config == nil {
        return nil, nil
    }

    var match *image.Image
    for _, id := range imageStore.Children(parentID) {
        img, err := imageStore.Get(id)
        if err != nil {
            return nil, fmt.Errorf("unable to find image %q", id)
        }

        builtLocally, err := imageStore.IsBuiltLocally(id)
        if err != nil {
            log.G(context.TODO()).WithFields(log.Fields{
                "error": err,
                "id":    id,
            }).Warn("failed to check if image was built locally")
            continue
        }
        if !builtLocally {
            continue
        }

        imgPlatform := img.Platform()
        // Discard old linux/amd64 images with empty platform.
        if imgPlatform.OS == "" && imgPlatform.Architecture == "" {
            continue
        }
        if !comparePlatform(platform, imgPlatform) {
            continue
        }

        if compare(&img.ContainerConfig, config) {
            // check for the most up to date match
            if img.Created != nil && (match == nil || match.Created.Before(*img.Created)) {
                match = img
            }
        }
    }
    return match, nil
}