moby/image/store.go
Sebastiaan van Stijn a3c97beee0
image: implement CheckOS, deprecate pkg/system IsOSSupported
Implement a function that returns an error to replace existing uses of
the IsOSSupported utility, where callers had to produce the error after
checking.

The IsOSSupported function was used in combination with images, so implementing
a utility in "image" to prevent having to import pkg/system (which contains many
unrelated functions)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-09-07 22:14:44 +02:00

347 lines
8.3 KiB
Go

package image // import "github.com/docker/docker/image"
import (
"context"
"fmt"
"sync"
"time"
"github.com/containerd/containerd/log"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/layer"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/go-digest/digestset"
"github.com/pkg/errors"
)
// Store is an interface for creating and accessing images
type Store interface {
Create(config []byte) (ID, error)
Get(id ID) (*Image, error)
Delete(id ID) ([]layer.Metadata, error)
Search(partialID string) (ID, error)
SetParent(id ID, parent ID) error
GetParent(id ID) (ID, error)
SetLastUpdated(id ID) error
GetLastUpdated(id ID) (time.Time, error)
Children(id ID) []ID
Map() map[ID]*Image
Heads() map[ID]*Image
Len() int
}
// LayerGetReleaser is a minimal interface for getting and releasing images.
type LayerGetReleaser interface {
Get(layer.ChainID) (layer.Layer, error)
Release(layer.Layer) ([]layer.Metadata, error)
}
type imageMeta struct {
layer layer.Layer
children map[ID]struct{}
}
type store struct {
sync.RWMutex
lss LayerGetReleaser
images map[ID]*imageMeta
fs StoreBackend
digestSet *digestset.Set
}
// NewImageStore returns new store object for given set of layer stores
func NewImageStore(fs StoreBackend, lss LayerGetReleaser) (Store, error) {
is := &store{
lss: lss,
images: make(map[ID]*imageMeta),
fs: fs,
digestSet: digestset.NewSet(),
}
// load all current images and retain layers
if err := is.restore(); err != nil {
return nil, err
}
return is, nil
}
func (is *store) restore() error {
// As the code below is run when restoring all images (which can be "many"),
// constructing the "log.G(ctx).WithFields" is deliberately not "DRY", as the
// logger is only used for error-cases, and we don't want to do allocations
// if we don't need it. The "f" type alias is here is just for convenience,
// and to make the code _slightly_ more DRY. See the discussion on GitHub;
// https://github.com/moby/moby/pull/44426#discussion_r1059519071
type f = log.Fields
err := is.fs.Walk(func(dgst digest.Digest) error {
img, err := is.Get(ID(dgst))
if err != nil {
log.G(context.TODO()).WithFields(f{"digest": dgst, "err": err}).Error("invalid image")
return nil
}
var l layer.Layer
if chainID := img.RootFS.ChainID(); chainID != "" {
if err := CheckOS(img.OperatingSystem()); err != nil {
log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem()}).Error("not restoring image with unsupported operating system")
return nil
}
l, err = is.lss.Get(chainID)
if err != nil {
if errors.Is(err, layer.ErrLayerDoesNotExist) {
log.G(context.TODO()).WithFields(f{"chainID": chainID, "os": img.OperatingSystem(), "err": err}).Error("not restoring image")
return nil
}
return err
}
}
if err := is.digestSet.Add(dgst); err != nil {
return err
}
is.images[ID(dgst)] = &imageMeta{
layer: l,
children: make(map[ID]struct{}),
}
return nil
})
if err != nil {
return err
}
// Second pass to fill in children maps
for id := range is.images {
if parent, err := is.GetParent(id); err == nil {
if parentMeta := is.images[parent]; parentMeta != nil {
parentMeta.children[id] = struct{}{}
}
}
}
return nil
}
func (is *store) Create(config []byte) (ID, error) {
var img *Image
img, err := NewFromJSON(config)
if err != nil {
return "", err
}
// Must reject any config that references diffIDs from the history
// which aren't among the rootfs layers.
rootFSLayers := make(map[layer.DiffID]struct{})
for _, diffID := range img.RootFS.DiffIDs {
rootFSLayers[diffID] = struct{}{}
}
layerCounter := 0
for _, h := range img.History {
if !h.EmptyLayer {
layerCounter++
}
}
if layerCounter > len(img.RootFS.DiffIDs) {
return "", errdefs.InvalidParameter(errors.New("too many non-empty layers in History section"))
}
imageDigest, err := is.fs.Set(config)
if err != nil {
return "", errdefs.InvalidParameter(err)
}
is.Lock()
defer is.Unlock()
imageID := ID(imageDigest)
if _, exists := is.images[imageID]; exists {
return imageID, nil
}
layerID := img.RootFS.ChainID()
var l layer.Layer
if layerID != "" {
if err := CheckOS(img.OperatingSystem()); err != nil {
return "", err
}
l, err = is.lss.Get(layerID)
if err != nil {
return "", errdefs.InvalidParameter(errors.Wrapf(err, "failed to get layer %s", layerID))
}
}
is.images[imageID] = &imageMeta{
layer: l,
children: make(map[ID]struct{}),
}
if err = is.digestSet.Add(imageDigest); err != nil {
delete(is.images, imageID)
return "", errdefs.InvalidParameter(err)
}
return imageID, nil
}
type imageNotFoundError string
func (e imageNotFoundError) Error() string {
return "No such image: " + string(e)
}
func (imageNotFoundError) NotFound() {}
func (is *store) Search(term string) (ID, error) {
dgst, err := is.digestSet.Lookup(term)
if err != nil {
if err == digestset.ErrDigestNotFound {
err = imageNotFoundError(term)
}
return "", errors.WithStack(err)
}
return ID(dgst), nil
}
func (is *store) Get(id ID) (*Image, error) {
// todo: Check if image is in images
// todo: Detect manual insertions and start using them
config, err := is.fs.Get(id.Digest())
if err != nil {
return nil, errdefs.NotFound(err)
}
img, err := NewFromJSON(config)
if err != nil {
return nil, errdefs.InvalidParameter(err)
}
img.computedID = id
img.Parent, err = is.GetParent(id)
if err != nil {
img.Parent = ""
}
return img, nil
}
func (is *store) Delete(id ID) ([]layer.Metadata, error) {
is.Lock()
defer is.Unlock()
imgMeta := is.images[id]
if imgMeta == nil {
return nil, errdefs.NotFound(fmt.Errorf("unrecognized image ID %s", id.String()))
}
_, err := is.Get(id)
if err != nil {
return nil, errdefs.NotFound(fmt.Errorf("unrecognized image %s, %v", id.String(), err))
}
for cID := range imgMeta.children {
is.fs.DeleteMetadata(cID.Digest(), "parent")
}
if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
delete(is.images[parent].children, id)
}
if err := is.digestSet.Remove(id.Digest()); err != nil {
log.G(context.TODO()).Errorf("error removing %s from digest set: %q", id, err)
}
delete(is.images, id)
is.fs.Delete(id.Digest())
if imgMeta.layer != nil {
return is.lss.Release(imgMeta.layer)
}
return nil, nil
}
func (is *store) SetParent(id, parentID ID) error {
is.Lock()
defer is.Unlock()
parentMeta := is.images[parentID]
if parentMeta == nil {
return errdefs.NotFound(fmt.Errorf("unknown parent image ID %s", parentID.String()))
}
if parent, err := is.GetParent(id); err == nil && is.images[parent] != nil {
delete(is.images[parent].children, id)
}
parentMeta.children[id] = struct{}{}
return is.fs.SetMetadata(id.Digest(), "parent", []byte(parentID))
}
func (is *store) GetParent(id ID) (ID, error) {
d, err := is.fs.GetMetadata(id.Digest(), "parent")
if err != nil {
return "", errdefs.NotFound(err)
}
return ID(d), nil // todo: validate?
}
// SetLastUpdated time for the image ID to the current time
func (is *store) SetLastUpdated(id ID) error {
lastUpdated := []byte(time.Now().Format(time.RFC3339Nano))
return is.fs.SetMetadata(id.Digest(), "lastUpdated", lastUpdated)
}
// GetLastUpdated time for the image ID
func (is *store) GetLastUpdated(id ID) (time.Time, error) {
bytes, err := is.fs.GetMetadata(id.Digest(), "lastUpdated")
if err != nil || len(bytes) == 0 {
// No lastUpdated time
return time.Time{}, nil
}
return time.Parse(time.RFC3339Nano, string(bytes))
}
func (is *store) Children(id ID) []ID {
is.RLock()
defer is.RUnlock()
return is.children(id)
}
func (is *store) children(id ID) []ID {
var ids []ID
if is.images[id] != nil {
for id := range is.images[id].children {
ids = append(ids, id)
}
}
return ids
}
func (is *store) Heads() map[ID]*Image {
return is.imagesMap(false)
}
func (is *store) Map() map[ID]*Image {
return is.imagesMap(true)
}
func (is *store) imagesMap(all bool) map[ID]*Image {
is.RLock()
defer is.RUnlock()
images := make(map[ID]*Image)
for id := range is.images {
if !all && len(is.children(id)) > 0 {
continue
}
img, err := is.Get(id)
if err != nil {
log.G(context.TODO()).Errorf("invalid image access: %q, error: %q", id, err)
continue
}
images[id] = img
}
return images
}
func (is *store) Len() int {
is.RLock()
defer is.RUnlock()
return len(is.images)
}